diff --git a/assets/controllers/elements/collection_type_controller.js b/assets/controllers/elements/collection_type_controller.js index 14b683e0..647ed5e5 100644 --- a/assets/controllers/elements/collection_type_controller.js +++ b/assets/controllers/elements/collection_type_controller.js @@ -74,15 +74,33 @@ export default class extends Controller { const newElementStr = this.htmlDecode(prototype.replace(regex, this.generateUID())); + let ret = null; + //Insert new html after the last child element //If the table has a tbody, insert it there //Afterwards return the newly created row if(targetTable.tBodies[0]) { targetTable.tBodies[0].insertAdjacentHTML('beforeend', newElementStr); - return targetTable.tBodies[0].lastElementChild; + ret = targetTable.tBodies[0].lastElementChild; } else { //Otherwise just insert it targetTable.insertAdjacentHTML('beforeend', newElementStr); - return targetTable.lastElementChild; + ret = targetTable.lastElementChild; + } + + //Trigger an event to notify other components that a new element has been created, so they can for example initialize select2 on it + targetTable.dispatchEvent(new CustomEvent("collection:elementAdded", {bubbles: true})); + + this.focusNumberInput(ret); + + return ret; + + } + + focusNumberInput(element) { + const fields = element.querySelectorAll("input[type=number]"); + //Focus the first available number input field to open the numeric keyboard on mobile devices + if(fields.length > 0) { + fields[0].focus(); } } diff --git a/assets/controllers/pages/part_stocktake_modal_controller.js b/assets/controllers/pages/part_stocktake_modal_controller.js new file mode 100644 index 00000000..7aef2906 --- /dev/null +++ b/assets/controllers/pages/part_stocktake_modal_controller.js @@ -0,0 +1,27 @@ +import {Controller} from "@hotwired/stimulus"; +import {Modal} from "bootstrap"; + +export default class extends Controller +{ + connect() { + this.element.addEventListener('show.bs.modal', event => this._handleModalOpen(event)); + } + + _handleModalOpen(event) { + // Button that triggered the modal + const button = event.relatedTarget; + + const amountInput = this.element.querySelector('input[name="amount"]'); + + // Extract info from button attributes + const lotID = button.getAttribute('data-lot-id'); + const lotAmount = button.getAttribute('data-lot-amount'); + + //Find the expected amount field and set the value to the lot amount + const expectedAmountInput = this.element.querySelector('#stocktake-modal-expected-amount'); + expectedAmountInput.textContent = lotAmount; + + //Set the action and lotID inputs in the form + this.element.querySelector('input[name="lot_id"]').setAttribute('value', lotID); + } +} diff --git a/assets/controllers/pages/part_withdraw_modal_controller.js b/assets/controllers/pages/part_withdraw_modal_controller.js index 2d6742b4..0e5c0fc5 100644 --- a/assets/controllers/pages/part_withdraw_modal_controller.js +++ b/assets/controllers/pages/part_withdraw_modal_controller.js @@ -5,6 +5,7 @@ export default class extends Controller { connect() { this.element.addEventListener('show.bs.modal', event => this._handleModalOpen(event)); + this.element.addEventListener('shown.bs.modal', event => this._handleModalShown(event)); } _handleModalOpen(event) { @@ -61,4 +62,8 @@ export default class extends Controller amountInput.setAttribute('max', lotAmount); } } + + _handleModalShown(event) { + this.element.querySelector('input[name="amount"]').focus(); + } } \ No newline at end of file diff --git a/assets/js/tristate_checkboxes.js b/assets/js/tristate_checkboxes.js index 4cf4fc1e..467099ab 100644 --- a/assets/js/tristate_checkboxes.js +++ b/assets/js/tristate_checkboxes.js @@ -56,7 +56,8 @@ class TristateHelper { document.addEventListener("turbo:load", listener); document.addEventListener("turbo:render", listener); + document.addEventListener("collection:elementAdded", listener); } } -export default new TristateHelper(); \ No newline at end of file +export default new TristateHelper(); diff --git a/composer.json b/composer.json index 36dd461e..89e0f19b 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "api-platform/json-api": "^4.0.0", "api-platform/symfony": "^4.0.0", "beberlei/doctrineextensions": "^1.2", - "brick/math": "^0.13.1", + "brick/math": "^0.14.8", "brick/schema": "^0.2.0", "composer/ca-bundle": "^1.5", "composer/package-versions-deprecated": "^1.11.99.5", @@ -28,7 +28,7 @@ "doctrine/orm": "^3.2.0", "dompdf/dompdf": "^3.1.2", "gregwar/captcha-bundle": "^2.1.0", - "hshn/base64-encoded-file": "^5.0", + "hshn/base64-encoded-file": "^6.0", "jbtronics/2fa-webauthn": "^3.0.0", "jbtronics/dompdf-font-loader-bundle": "^1.0.0", "jbtronics/settings-bundle": "^3.0.0", @@ -45,7 +45,6 @@ "nelmio/security-bundle": "^3.0", "nyholm/psr7": "^1.1", "omines/datatables-bundle": "^0.10.0", - "paragonie/sodium_compat": "^1.21", "part-db/label-fonts": "^1.0", "part-db/swap-bundle": "^6.0.0", "phpoffice/phpspreadsheet": "^5.0.0", @@ -70,7 +69,7 @@ "symfony/http-client": "7.4.*", "symfony/http-kernel": "7.4.*", "symfony/mailer": "7.4.*", - "symfony/monolog-bundle": "^3.1", + "symfony/monolog-bundle": "^4.0", "symfony/process": "7.4.*", "symfony/property-access": "7.4.*", "symfony/property-info": "7.4.*", @@ -88,8 +87,9 @@ "symfony/web-link": "7.4.*", "symfony/webpack-encore-bundle": "^v2.0.1", "symfony/yaml": "7.4.*", - "symplify/easy-coding-standard": "^12.5.20", + "symplify/easy-coding-standard": "^13.0", "tecnickcom/tc-lib-barcode": "^2.1.4", + "tiendanube/gtinvalidation": "^1.0", "twig/cssinliner-extra": "^3.0", "twig/extra-bundle": "^3.8", "twig/html-extra": "^3.8", @@ -128,7 +128,7 @@ }, "suggest": { "ext-bcmath": "Used to improve price calculation performance", - "ext-gmp": "Used to improve price calculation performanice" + "ext-gmp": "Used to improve price calculation performance" }, "config": { "preferred-install": { diff --git a/composer.lock b/composer.lock index b56a61f9..2db828a5 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": "7ca9c95fb85f6bf3d9b8a3aa98ca33f6", + "content-hash": "32c5677a31185e0ed124904012500154", "packages": [ { "name": "amphp/amp", @@ -968,16 +968,16 @@ }, { "name": "api-platform/doctrine-common", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-common.git", - "reference": "4967ed6ba91465d6a6a047119658984d40f89a0e" + "reference": "566acb646b001f21bc6aa7bd36a109e075f5c131" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/4967ed6ba91465d6a6a047119658984d40f89a0e", - "reference": "4967ed6ba91465d6a6a047119658984d40f89a0e", + "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/566acb646b001f21bc6aa7bd36a109e075f5c131", + "reference": "566acb646b001f21bc6aa7bd36a109e075f5c131", "shasum": "" }, "require": { @@ -1052,22 +1052,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-common/tree/v4.2.15" + "source": "https://github.com/api-platform/doctrine-common/tree/v4.2.16" }, - "time": "2026-01-27T07:12:16+00:00" + "time": "2026-02-13T15:07:33+00:00" }, { "name": "api-platform/doctrine-orm", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-orm.git", - "reference": "cf5c99a209a7be3e508c6f5d0fa4d853d43cff84" + "reference": "99d8b8cdee4ca79fd3abb351991bda6b42696eee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/cf5c99a209a7be3e508c6f5d0fa4d853d43cff84", - "reference": "cf5c99a209a7be3e508c6f5d0fa4d853d43cff84", + "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/99d8b8cdee4ca79fd3abb351991bda6b42696eee", + "reference": "99d8b8cdee4ca79fd3abb351991bda6b42696eee", "shasum": "" }, "require": { @@ -1139,13 +1139,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-orm/tree/v4.2.15" + "source": "https://github.com/api-platform/doctrine-orm/tree/v4.2.16" }, - "time": "2026-01-26T15:38:30+00:00" + "time": "2026-02-13T17:30:49+00:00" }, { "name": "api-platform/documentation", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/documentation.git", @@ -1202,22 +1202,22 @@ ], "description": "API Platform documentation controller.", "support": { - "source": "https://github.com/api-platform/documentation/tree/v4.2.15" + "source": "https://github.com/api-platform/documentation/tree/v4.2.16" }, "time": "2025-12-27T22:15:57+00:00" }, { "name": "api-platform/http-cache", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/http-cache.git", - "reference": "04a9239b67425f68ed2d372c2c731f14342dea45" + "reference": "ec5f9068d3d66be63db4d80acaf518868dea1321" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/http-cache/zipball/04a9239b67425f68ed2d372c2c731f14342dea45", - "reference": "04a9239b67425f68ed2d372c2c731f14342dea45", + "url": "https://api.github.com/repos/api-platform/http-cache/zipball/ec5f9068d3d66be63db4d80acaf518868dea1321", + "reference": "ec5f9068d3d66be63db4d80acaf518868dea1321", "shasum": "" }, "require": { @@ -1282,22 +1282,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/http-cache/tree/v4.2.15" + "source": "https://github.com/api-platform/http-cache/tree/v4.2.16" }, - "time": "2026-01-12T13:36:15+00:00" + "time": "2026-02-13T15:07:33+00:00" }, { "name": "api-platform/hydra", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/hydra.git", - "reference": "32ca5ff3ac5197d0606a846a6570127239091422" + "reference": "ddba613f615caa8372df3d478a36a910b77f6d28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/hydra/zipball/32ca5ff3ac5197d0606a846a6570127239091422", - "reference": "32ca5ff3ac5197d0606a846a6570127239091422", + "url": "https://api.github.com/repos/api-platform/hydra/zipball/ddba613f615caa8372df3d478a36a910b77f6d28", + "reference": "ddba613f615caa8372df3d478a36a910b77f6d28", "shasum": "" }, "require": { @@ -1369,22 +1369,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/hydra/tree/v4.2.15" + "source": "https://github.com/api-platform/hydra/tree/v4.2.16" }, - "time": "2026-01-30T09:06:20+00:00" + "time": "2026-02-13T15:07:33+00:00" }, { "name": "api-platform/json-api", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/json-api.git", - "reference": "32ca38f977203f8a59f6efee9637261ae4651c29" + "reference": "6c5b5b83f693667371b7b31a65a50925e10c6d46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-api/zipball/32ca38f977203f8a59f6efee9637261ae4651c29", - "reference": "32ca38f977203f8a59f6efee9637261ae4651c29", + "url": "https://api.github.com/repos/api-platform/json-api/zipball/6c5b5b83f693667371b7b31a65a50925e10c6d46", + "reference": "6c5b5b83f693667371b7b31a65a50925e10c6d46", "shasum": "" }, "require": { @@ -1451,22 +1451,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/json-api/tree/v4.2.15" + "source": "https://github.com/api-platform/json-api/tree/v4.2.16" }, - "time": "2026-01-26T15:38:30+00:00" + "time": "2026-02-13T17:30:49+00:00" }, { "name": "api-platform/json-schema", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/json-schema.git", - "reference": "4487398c59a07beefeec870a1213c34ae362cb00" + "reference": "3569ab8e3e5c01d77f00964683254809571fa078" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-schema/zipball/4487398c59a07beefeec870a1213c34ae362cb00", - "reference": "4487398c59a07beefeec870a1213c34ae362cb00", + "url": "https://api.github.com/repos/api-platform/json-schema/zipball/3569ab8e3e5c01d77f00964683254809571fa078", + "reference": "3569ab8e3e5c01d77f00964683254809571fa078", "shasum": "" }, "require": { @@ -1532,22 +1532,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/json-schema/tree/v4.2.15" + "source": "https://github.com/api-platform/json-schema/tree/v4.2.16" }, - "time": "2026-01-26T15:38:30+00:00" + "time": "2026-02-13T15:07:33+00:00" }, { "name": "api-platform/jsonld", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/jsonld.git", - "reference": "ef0a361b0f29158243478d3fff5038ec2f5aa76c" + "reference": "08593fc073466badae67b8f4999ec19e3ade9eab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/jsonld/zipball/ef0a361b0f29158243478d3fff5038ec2f5aa76c", - "reference": "ef0a361b0f29158243478d3fff5038ec2f5aa76c", + "url": "https://api.github.com/repos/api-platform/jsonld/zipball/08593fc073466badae67b8f4999ec19e3ade9eab", + "reference": "08593fc073466badae67b8f4999ec19e3ade9eab", "shasum": "" }, "require": { @@ -1612,22 +1612,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/jsonld/tree/v4.2.15" + "source": "https://github.com/api-platform/jsonld/tree/v4.2.16" }, - "time": "2026-01-12T13:36:15+00:00" + "time": "2026-02-13T17:30:49+00:00" }, { "name": "api-platform/metadata", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/metadata.git", - "reference": "4d10dbd7b8f036d24df35eb3ec02c0f0befcf397" + "reference": "f90cd4258477821e0174788a6666507824c7c6b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/metadata/zipball/4d10dbd7b8f036d24df35eb3ec02c0f0befcf397", - "reference": "4d10dbd7b8f036d24df35eb3ec02c0f0befcf397", + "url": "https://api.github.com/repos/api-platform/metadata/zipball/f90cd4258477821e0174788a6666507824c7c6b9", + "reference": "f90cd4258477821e0174788a6666507824c7c6b9", "shasum": "" }, "require": { @@ -1710,13 +1710,13 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/metadata/tree/v4.2.15" + "source": "https://github.com/api-platform/metadata/tree/v4.2.16" }, - "time": "2026-01-27T07:12:16+00:00" + "time": "2026-02-13T15:07:33+00:00" }, { "name": "api-platform/openapi", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/openapi.git", @@ -1800,22 +1800,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/openapi/tree/v4.2.15" + "source": "https://github.com/api-platform/openapi/tree/v4.2.16" }, "time": "2026-01-26T15:38:30+00:00" }, { "name": "api-platform/serializer", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/serializer.git", - "reference": "4d45483a9911b598a262dd2035166ab2040e430f" + "reference": "e01024d458c26d230eafbe8ac79dc8e28c3dc379" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/serializer/zipball/4d45483a9911b598a262dd2035166ab2040e430f", - "reference": "4d45483a9911b598a262dd2035166ab2040e430f", + "url": "https://api.github.com/repos/api-platform/serializer/zipball/e01024d458c26d230eafbe8ac79dc8e28c3dc379", + "reference": "e01024d458c26d230eafbe8ac79dc8e28c3dc379", "shasum": "" }, "require": { @@ -1893,22 +1893,22 @@ "serializer" ], "support": { - "source": "https://github.com/api-platform/serializer/tree/v4.2.15" + "source": "https://github.com/api-platform/serializer/tree/v4.2.16" }, - "time": "2026-01-26T15:38:30+00:00" + "time": "2026-02-13T17:30:49+00:00" }, { "name": "api-platform/state", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/state.git", - "reference": "89c0999206b4885c2e55204751b4db07061f3fd3" + "reference": "0fcd612696acac4632a626bb5dfc6bd99ec3b44a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/state/zipball/89c0999206b4885c2e55204751b4db07061f3fd3", - "reference": "89c0999206b4885c2e55204751b4db07061f3fd3", + "url": "https://api.github.com/repos/api-platform/state/zipball/0fcd612696acac4632a626bb5dfc6bd99ec3b44a", + "reference": "0fcd612696acac4632a626bb5dfc6bd99ec3b44a", "shasum": "" }, "require": { @@ -1990,22 +1990,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/state/tree/v4.2.15" + "source": "https://github.com/api-platform/state/tree/v4.2.16" }, - "time": "2026-01-26T15:38:30+00:00" + "time": "2026-02-13T15:07:33+00:00" }, { "name": "api-platform/symfony", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/symfony.git", - "reference": "93fdcbe189a1866412f5da04e26fa5615e99b210" + "reference": "769f5bc29ce59a5c68006ca5876c409072340e92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/symfony/zipball/93fdcbe189a1866412f5da04e26fa5615e99b210", - "reference": "93fdcbe189a1866412f5da04e26fa5615e99b210", + "url": "https://api.github.com/repos/api-platform/symfony/zipball/769f5bc29ce59a5c68006ca5876c409072340e92", + "reference": "769f5bc29ce59a5c68006ca5876c409072340e92", "shasum": "" }, "require": { @@ -2118,13 +2118,13 @@ "symfony" ], "support": { - "source": "https://github.com/api-platform/symfony/tree/v4.2.15" + "source": "https://github.com/api-platform/symfony/tree/v4.2.16" }, - "time": "2026-01-30T13:31:50+00:00" + "time": "2026-02-13T17:30:49+00:00" }, { "name": "api-platform/validator", - "version": "v4.2.15", + "version": "v4.2.16", "source": { "type": "git", "url": "https://github.com/api-platform/validator.git", @@ -2194,7 +2194,7 @@ "validator" ], "support": { - "source": "https://github.com/api-platform/validator/tree/v4.2.15" + "source": "https://github.com/api-platform/validator/tree/v4.2.16" }, "time": "2026-01-26T15:45:40+00:00" }, @@ -2329,25 +2329,25 @@ }, { "name": "brick/math", - "version": "0.13.1", + "version": "0.14.8", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", - "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.2" }, "require-dev": { "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^10.1", - "vimeo/psalm": "6.8.8" + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" }, "type": "library", "autoload": { @@ -2377,7 +2377,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.13.1" + "source": "https://github.com/brick/math/tree/0.14.8" }, "funding": [ { @@ -2385,7 +2385,7 @@ "type": "github" } ], - "time": "2025-03-29T13:50:30+00:00" + "time": "2026-02-10T14:33:43+00:00" }, { "name": "brick/schema", @@ -3791,16 +3791,16 @@ }, { "name": "doctrine/migrations", - "version": "3.9.5", + "version": "3.9.6", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "1b823afbc40f932dae8272574faee53f2755eac5" + "reference": "ffd8355cdd8505fc650d9604f058bf62aedd80a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/1b823afbc40f932dae8272574faee53f2755eac5", - "reference": "1b823afbc40f932dae8272574faee53f2755eac5", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/ffd8355cdd8505fc650d9604f058bf62aedd80a1", + "reference": "ffd8355cdd8505fc650d9604f058bf62aedd80a1", "shasum": "" }, "require": { @@ -3874,7 +3874,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.9.5" + "source": "https://github.com/doctrine/migrations/tree/3.9.6" }, "funding": [ { @@ -3890,7 +3890,7 @@ "type": "tidelift" } ], - "time": "2025-11-20T11:15:36+00:00" + "time": "2026-02-11T06:46:11+00:00" }, { "name": "doctrine/orm", @@ -4075,16 +4075,16 @@ }, { "name": "doctrine/sql-formatter", - "version": "1.5.3", + "version": "1.5.4", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7" + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/a8af23a8e9d622505baa2997465782cbe8bb7fc7", - "reference": "a8af23a8e9d622505baa2997465782cbe8bb7fc7", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/9563949f5cd3bd12a17d12fb980528bc141c5806", + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806", "shasum": "" }, "require": { @@ -4124,9 +4124,9 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.5.3" + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.4" }, - "time": "2025-10-26T09:35:14+00:00" + "time": "2026-02-08T16:21:46+00:00" }, { "name": "dompdf/dompdf", @@ -4869,30 +4869,30 @@ }, { "name": "hshn/base64-encoded-file", - "version": "v5.0.3", + "version": "v6.0.0", "source": { "type": "git", "url": "https://github.com/hshn/base64-encoded-file.git", - "reference": "74984c7e69fbed9378dbf1d64e632522cc1b6d95" + "reference": "f379875f5582ebcda20111bb68d6a3268dddf345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hshn/base64-encoded-file/zipball/74984c7e69fbed9378dbf1d64e632522cc1b6d95", - "reference": "74984c7e69fbed9378dbf1d64e632522cc1b6d95", + "url": "https://api.github.com/repos/hshn/base64-encoded-file/zipball/f379875f5582ebcda20111bb68d6a3268dddf345", + "reference": "f379875f5582ebcda20111bb68d6a3268dddf345", "shasum": "" }, "require": { "php": "^8.1.0", - "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0", - "symfony/mime": "^5.4 || ^6.0 || ^7.0" + "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/mime": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0.0", - "symfony/config": "^5.4 || ^6.0 || ^7.0", - "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", - "symfony/form": "^5.4 || ^6.0 || ^7.0", - "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", - "symfony/serializer": "^5.4 || ^6.0 || ^7.0" + "symfony/config": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/form": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/serializer": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "suggest": { "symfony/config": "to use the bundle in a Symfony project", @@ -4904,7 +4904,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.x-dev" + "dev-master": "6.x-dev" } }, "autoload": { @@ -4925,9 +4925,9 @@ "description": "Provides handling base64 encoded files, and the integration of symfony/form", "support": { "issues": "https://github.com/hshn/base64-encoded-file/issues", - "source": "https://github.com/hshn/base64-encoded-file/tree/v5.0.3" + "source": "https://github.com/hshn/base64-encoded-file/tree/v6.0.0" }, - "time": "2025-10-06T10:34:52+00:00" + "time": "2025-12-05T15:24:18+00:00" }, { "name": "imagine/imagine", @@ -5489,16 +5489,16 @@ }, { "name": "knpuniversity/oauth2-client-bundle", - "version": "v2.20.1", + "version": "v2.20.2", "source": { "type": "git", "url": "https://github.com/knpuniversity/oauth2-client-bundle.git", - "reference": "d59e4dc61484e777b6f19df2efcf8b1bcc03828a" + "reference": "9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/d59e4dc61484e777b6f19df2efcf8b1bcc03828a", - "reference": "d59e4dc61484e777b6f19df2efcf8b1bcc03828a", + "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7", + "reference": "9ce4fcea69dbbf4d19ee7368b8d623ec2d73d3c7", "shasum": "" }, "require": { @@ -5543,9 +5543,9 @@ ], "support": { "issues": "https://github.com/knpuniversity/oauth2-client-bundle/issues", - "source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.20.1" + "source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.20.2" }, - "time": "2025-12-04T15:46:43+00:00" + "time": "2026-02-12T17:07:18+00:00" }, { "name": "lcobucci/clock", @@ -7237,16 +7237,16 @@ }, { "name": "nette/schema", - "version": "v1.3.3", + "version": "v1.3.4", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004" + "reference": "086497a2f34b82fede9b5a41cc8e131d087cd8f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004", - "reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004", + "url": "https://api.github.com/repos/nette/schema/zipball/086497a2f34b82fede9b5a41cc8e131d087cd8f7", + "reference": "086497a2f34b82fede9b5a41cc8e131d087cd8f7", "shasum": "" }, "require": { @@ -7254,8 +7254,8 @@ "php": "8.1 - 8.5" }, "require-dev": { - "nette/tester": "^2.5.2", - "phpstan/phpstan-nette": "^2.0@stable", + "nette/tester": "^2.6", + "phpstan/phpstan": "^2.0@stable", "tracy/tracy": "^2.8" }, "type": "library", @@ -7296,22 +7296,22 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.3" + "source": "https://github.com/nette/schema/tree/v1.3.4" }, - "time": "2025-10-30T22:57:59+00:00" + "time": "2026-02-08T02:54:00+00:00" }, { "name": "nette/utils", - "version": "v4.1.2", + "version": "v4.1.3", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5" + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/f76b5dc3d6c6d3043c8d937df2698515b99cbaf5", - "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5", + "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", "shasum": "" }, "require": { @@ -7323,8 +7323,10 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", "nette/tester": "^2.5", - "phpstan/phpstan": "^2.0@stable", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", "tracy/tracy": "^2.9" }, "suggest": { @@ -7385,9 +7387,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.2" + "source": "https://github.com/nette/utils/tree/v4.1.3" }, - "time": "2026-02-03T17:21:09+00:00" + "time": "2026-02-13T03:05:33+00:00" }, { "name": "nikolaposa/version", @@ -7781,142 +7783,6 @@ }, "time": "2025-09-24T15:06:41+00:00" }, - { - "name": "paragonie/random_compat", - "version": "v9.99.100", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", - "shasum": "" - }, - "require": { - "php": ">= 7" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/random_compat/issues", - "source": "https://github.com/paragonie/random_compat" - }, - "time": "2020-10-15T08:29:30+00:00" - }, - { - "name": "paragonie/sodium_compat", - "version": "v1.24.0", - "source": { - "type": "git", - "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "2cb48f26130919f92f30650bdcc30e6f4ebe45ac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/2cb48f26130919f92f30650bdcc30e6f4ebe45ac", - "reference": "2cb48f26130919f92f30650bdcc30e6f4ebe45ac", - "shasum": "" - }, - "require": { - "paragonie/random_compat": ">=1", - "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" - }, - "require-dev": { - "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" - }, - "suggest": { - "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", - "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." - }, - "type": "library", - "autoload": { - "files": [ - "autoload.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com" - }, - { - "name": "Frank Denis", - "email": "jedisct1@pureftpd.org" - } - ], - "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", - "keywords": [ - "Authentication", - "BLAKE2b", - "ChaCha20", - "ChaCha20-Poly1305", - "Chapoly", - "Curve25519", - "Ed25519", - "EdDSA", - "Edwards-curve Digital Signature Algorithm", - "Elliptic Curve Diffie-Hellman", - "Poly1305", - "Pure-PHP cryptography", - "RFC 7748", - "RFC 8032", - "Salpoly", - "Salsa20", - "X25519", - "XChaCha20-Poly1305", - "XSalsa20-Poly1305", - "Xchacha20", - "Xsalsa20", - "aead", - "cryptography", - "ecdh", - "elliptic curve", - "elliptic curve cryptography", - "encryption", - "libsodium", - "php", - "public-key cryptography", - "secret-key cryptography", - "side-channel resistant" - ], - "support": { - "issues": "https://github.com/paragonie/sodium_compat/issues", - "source": "https://github.com/paragonie/sodium_compat/tree/v1.24.0" - }, - "time": "2025-12-30T16:16:35+00:00" - }, { "name": "part-db/exchanger", "version": "v3.1.0", @@ -7996,16 +7862,16 @@ }, { "name": "part-db/label-fonts", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/Part-DB/label-fonts.git", - "reference": "c85aeb051d6492961a2c59bc291979f15ce60e88" + "reference": "f93cbc8885c96792ab86f42d76e58cf8825975d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Part-DB/label-fonts/zipball/c85aeb051d6492961a2c59bc291979f15ce60e88", - "reference": "c85aeb051d6492961a2c59bc291979f15ce60e88", + "url": "https://api.github.com/repos/Part-DB/label-fonts/zipball/f93cbc8885c96792ab86f42d76e58cf8825975d9", + "reference": "f93cbc8885c96792ab86f42d76e58cf8825975d9", "shasum": "" }, "type": "library", @@ -8028,9 +7894,9 @@ ], "support": { "issues": "https://github.com/Part-DB/label-fonts/issues", - "source": "https://github.com/Part-DB/label-fonts/tree/v1.2.0" + "source": "https://github.com/Part-DB/label-fonts/tree/v1.3.0" }, - "time": "2025-09-07T15:42:51+00:00" + "time": "2026-02-14T21:44:31+00:00" }, { "name": "part-db/swap", @@ -12888,33 +12754,32 @@ }, { "name": "symfony/monolog-bundle", - "version": "v3.11.1", + "version": "v4.0.1", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bundle.git", - "reference": "0e675a6e08f791ef960dc9c7e392787111a3f0c1" + "reference": "3b4ee2717ee56c5e1edb516140a175eb2a72bc66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/0e675a6e08f791ef960dc9c7e392787111a3f0c1", - "reference": "0e675a6e08f791ef960dc9c7e392787111a3f0c1", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/3b4ee2717ee56c5e1edb516140a175eb2a72bc66", + "reference": "3b4ee2717ee56c5e1edb516140a175eb2a72bc66", "shasum": "" }, "require": { "composer-runtime-api": "^2.0", - "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", - "php": ">=8.1", - "symfony/config": "^6.4 || ^7.0", - "symfony/dependency-injection": "^6.4 || ^7.0", - "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/http-kernel": "^6.4 || ^7.0", - "symfony/monolog-bridge": "^6.4 || ^7.0", + "monolog/monolog": "^3.5", + "php": ">=8.2", + "symfony/config": "^7.3 || ^8.0", + "symfony/dependency-injection": "^7.3 || ^8.0", + "symfony/http-kernel": "^7.3 || ^8.0", + "symfony/monolog-bridge": "^7.3 || ^8.0", "symfony/polyfill-php84": "^1.30" }, "require-dev": { - "symfony/console": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^7.3.3", - "symfony/yaml": "^6.4 || ^7.0" + "phpunit/phpunit": "^11.5.41 || ^12.3", + "symfony/console": "^7.3 || ^8.0", + "symfony/yaml": "^7.3 || ^8.0" }, "type": "symfony-bundle", "autoload": { @@ -12944,7 +12809,7 @@ ], "support": { "issues": "https://github.com/symfony/monolog-bundle/issues", - "source": "https://github.com/symfony/monolog-bundle/tree/v3.11.1" + "source": "https://github.com/symfony/monolog-bundle/tree/v4.0.1" }, "funding": [ { @@ -12964,7 +12829,7 @@ "type": "tidelift" } ], - "time": "2025-12-08T07:58:26+00:00" + "time": "2025-12-08T08:00:13+00:00" }, { "name": "symfony/options-resolver", @@ -16461,24 +16326,24 @@ }, { "name": "symplify/easy-coding-standard", - "version": "12.6.2", + "version": "13.0.4", "source": { "type": "git", "url": "https://github.com/easy-coding-standard/easy-coding-standard.git", - "reference": "7a6798aa424f0ecafb1542b6f5207c5a99704d3d" + "reference": "5c7e7a07e5d6a98b9dd2e6fc0a9155efb7c166c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/7a6798aa424f0ecafb1542b6f5207c5a99704d3d", - "reference": "7a6798aa424f0ecafb1542b6f5207c5a99704d3d", + "url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/5c7e7a07e5d6a98b9dd2e6fc0a9155efb7c166c8", + "reference": "5c7e7a07e5d6a98b9dd2e6fc0a9155efb7c166c8", "shasum": "" }, "require": { "php": ">=7.2" }, "conflict": { - "friendsofphp/php-cs-fixer": "<3.46", - "phpcsstandards/php_codesniffer": "<3.8", + "friendsofphp/php-cs-fixer": "<3.92.4", + "phpcsstandards/php_codesniffer": "<4.0.1", "symplify/coding-standard": "<12.1" }, "suggest": { @@ -16506,7 +16371,7 @@ ], "support": { "issues": "https://github.com/easy-coding-standard/easy-coding-standard/issues", - "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/12.6.2" + "source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/13.0.4" }, "funding": [ { @@ -16518,7 +16383,7 @@ "type": "github" } ], - "time": "2025-10-29T08:51:50+00:00" + "time": "2026-01-05T09:10:04+00:00" }, { "name": "tecnickcom/tc-lib-barcode", @@ -16693,16 +16558,16 @@ }, { "name": "thecodingmachine/safe", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236" + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236", - "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", "shasum": "" }, "require": { @@ -16812,7 +16677,7 @@ "description": "PHP core functions that throw exceptions instead of returning FALSE on error", "support": { "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v3.3.0" + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" }, "funding": [ { @@ -16823,12 +16688,64 @@ "url": "https://github.com/shish", "type": "github" }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, { "url": "https://github.com/staabm", "type": "github" } ], - "time": "2025-05-14T06:15:44+00:00" + "time": "2026-02-04T18:08:13+00:00" + }, + { + "name": "tiendanube/gtinvalidation", + "version": "v1.0", + "source": { + "type": "git", + "url": "https://github.com/TiendaNube/GtinValidator.git", + "reference": "7ff5794b6293eb748bf1efcddf4e20a657c31855" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/TiendaNube/GtinValidator/zipball/7ff5794b6293eb748bf1efcddf4e20a657c31855", + "reference": "7ff5794b6293eb748bf1efcddf4e20a657c31855", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "4.6.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "GtinValidation\\": "src/GtinValidation" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ryan Blakemore", + "homepage": "https://github.com/ryanblak", + "role": "developer" + } + ], + "description": "Validates GTIN product codes.", + "keywords": [ + "gtin", + "product codes" + ], + "support": { + "issues": "https://github.com/TiendaNube/GtinValidator/issues", + "source": "https://github.com/TiendaNube/GtinValidator/tree/master" + }, + "time": "2018-07-25T22:31:29+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -17752,16 +17669,16 @@ }, { "name": "webmozart/assert", - "version": "2.1.2", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649" + "reference": "6976757ba8dd70bf8cbaea0914ad84d8b51a9f46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", - "reference": "ce6a2f100c404b2d32a1dd1270f9b59ad4f57649", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6976757ba8dd70bf8cbaea0914ad84d8b51a9f46", + "reference": "6976757ba8dd70bf8cbaea0914ad84d8b51a9f46", "shasum": "" }, "require": { @@ -17808,9 +17725,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.1.2" + "source": "https://github.com/webmozarts/assert/tree/2.1.3" }, - "time": "2026-01-13T14:02:24+00:00" + "time": "2026-02-13T21:01:40+00:00" }, { "name": "willdurand/negotiation", @@ -18447,11 +18364,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.38", + "version": "2.1.39", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629", - "reference": "dfaf1f530e1663aa167bc3e52197adb221582629", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224", + "reference": "c6f73a2af4cbcd99c931d0fb8f08548cc0fa8224", "shasum": "" }, "require": { @@ -18496,20 +18413,20 @@ "type": "github" } ], - "time": "2026-01-30T17:12:46+00:00" + "time": "2026-02-11T14:48:56+00:00" }, { "name": "phpstan/phpstan-doctrine", - "version": "2.0.14", + "version": "2.0.16", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "70cd3e82fef49171163ff682a89cfe793d88581c" + "reference": "f4ff6084a26d91174b3f0b047589af293a893104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/70cd3e82fef49171163ff682a89cfe793d88581c", - "reference": "70cd3e82fef49171163ff682a89cfe793d88581c", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/f4ff6084a26d91174b3f0b047589af293a893104", + "reference": "f4ff6084a26d91174b3f0b047589af293a893104", "shasum": "" }, "require": { @@ -18565,29 +18482,32 @@ "MIT" ], "description": "Doctrine extensions for PHPStan", + "keywords": [ + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.14" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.16" }, - "time": "2026-01-25T14:56:09+00:00" + "time": "2026-02-11T08:54:45+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "2.0.8", + "version": "2.0.10", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "1ed9e626a37f7067b594422411539aa807190573" + "reference": "1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/1ed9e626a37f7067b594422411539aa807190573", - "reference": "1ed9e626a37f7067b594422411539aa807190573", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f", + "reference": "1aba28b697c1e3b6bbec8a1725f8b11b6d3e5a5f", "shasum": "" }, "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.29" + "phpstan/phpstan": "^2.1.39" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -18613,24 +18533,27 @@ "MIT" ], "description": "Extra strict and opinionated rules for PHPStan", + "keywords": [ + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.8" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.10" }, - "time": "2026-01-27T08:10:25+00:00" + "time": "2026-02-11T14:17:32+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "2.0.12", + "version": "2.0.14", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "a46dd92eaf15146cd932d897a272e59cd4108ce2" + "reference": "678136545a552a33b07f1a59a013f76df286cc34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/a46dd92eaf15146cd932d897a272e59cd4108ce2", - "reference": "a46dd92eaf15146cd932d897a272e59cd4108ce2", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/678136545a552a33b07f1a59a013f76df286cc34", + "reference": "678136545a552a33b07f1a59a013f76df286cc34", "shasum": "" }, "require": { @@ -18684,11 +18607,14 @@ } ], "description": "Symfony Framework extensions and rules for PHPStan", + "keywords": [ + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.12" + "source": "https://github.com/phpstan/phpstan-symfony/tree/2.0.14" }, - "time": "2026-01-23T09:04:33+00:00" + "time": "2026-02-11T12:27:30+00:00" }, { "name": "phpunit/php-code-coverage", @@ -19039,16 +18965,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.51", + "version": "11.5.53", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "ad14159f92910b0f0e3928c13e9b2077529de091" + "reference": "a997a653a82845f1240d73ee73a8a4e97e4b0607" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad14159f92910b0f0e3928c13e9b2077529de091", - "reference": "ad14159f92910b0f0e3928c13e9b2077529de091", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a997a653a82845f1240d73ee73a8a4e97e4b0607", + "reference": "a997a653a82845f1240d73ee73a8a4e97e4b0607", "shasum": "" }, "require": { @@ -19121,7 +19047,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.51" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.53" }, "funding": [ { @@ -19145,7 +19071,7 @@ "type": "tidelift" } ], - "time": "2026-02-05T07:59:30+00:00" + "time": "2026-02-10T12:28:25+00:00" }, { "name": "rector/rector", @@ -19213,12 +19139,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "7ea2d110787f6807213e27a1255c6b858ad99d89" + "reference": "7f3e95c9ebf1b16e002dd2c913d30d962c2a6a16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7ea2d110787f6807213e27a1255c6b858ad99d89", - "reference": "7ea2d110787f6807213e27a1255c6b858ad99d89", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/7f3e95c9ebf1b16e002dd2c913d30d962c2a6a16", + "reference": "7f3e95c9ebf1b16e002dd2c913d30d962c2a6a16", "shasum": "" }, "conflict": { @@ -19249,6 +19175,7 @@ "amphp/artax": "<1.0.6|>=2,<2.0.6", "amphp/http": "<=1.7.2|>=2,<=2.1", "amphp/http-client": ">=4,<4.4", + "amphp/http-server": ">=2.0.0.0-RC1-dev,<2.1.10|>=3.0.0.0-beta1,<3.4.4", "anchorcms/anchor-cms": "<=0.12.7", "andreapollastri/cipi": "<=3.1.15", "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5", @@ -19325,6 +19252,7 @@ "causal/oidc": "<4", "cecil/cecil": "<7.47.1", "centreon/centreon": "<22.10.15", + "cesargb/laravel-magiclink": ">=2,<2.25.1", "cesnet/simplesamlphp-module-proxystatistics": "<3.1", "chriskacerguis/codeigniter-restserver": "<=2.7.1", "chrome-php/chrome": "<1.14", @@ -19359,9 +19287,10 @@ "cosenary/instagram": "<=2.3", "couleurcitron/tarteaucitron-wp": "<0.3", "cpsit/typo3-mailqueue": "<0.4.3|>=0.5,<0.5.1", - "craftcms/cms": "<=4.16.16|>=5,<=5.8.20", + "craftcms/cms": "<4.17.0.0-beta1|>=5,<5.9.0.0-beta1", "craftcms/commerce": ">=4.0.0.0-RC1-dev,<=4.10|>=5,<=5.5.1", "craftcms/composer": ">=4.0.0.0-RC1-dev,<=4.10|>=5.0.0.0-RC1-dev,<=5.5.1", + "craftcms/craft": ">=3.5,<=4.16.17|>=5.0.0.0-RC1-dev,<=5.8.21", "croogo/croogo": "<=4.0.7", "cuyz/valinor": "<0.12", "czim/file-handling": "<1.5|>=2,<2.3", @@ -19514,6 +19443,7 @@ "friendsoftypo3/mediace": ">=7.6.2,<7.6.5", "friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6", "froala/wysiwyg-editor": "<=4.3", + "frosh/adminer-platform": "<2.2.1", "froxlor/froxlor": "<=2.2.5", "frozennode/administrator": "<=5.0.12", "fuel/core": "<1.8.1", @@ -19564,7 +19494,7 @@ "ibexa/solr": ">=4.5,<4.5.4", "ibexa/user": ">=4,<4.4.3|>=5,<5.0.4", "icecoder/icecoder": "<=8.1", - "idno/known": "<=1.3.1", + "idno/known": "<=1.6.2", "ilicmiljan/secure-props": ">=1.2,<1.2.2", "illuminate/auth": "<5.5.10", "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<6.18.31|>=7,<7.22.4", @@ -19818,6 +19748,7 @@ "phpwhois/phpwhois": "<=4.2.5", "phpxmlrpc/extras": "<0.6.1", "phpxmlrpc/phpxmlrpc": "<4.9.2", + "phraseanet/phraseanet": "==4.0.3", "pi/pi": "<=2.5", "pimcore/admin-ui-classic-bundle": "<=1.7.15|>=2.0.0.0-RC1-dev,<=2.2.2", "pimcore/customer-management-framework-bundle": "<4.2.1", @@ -19955,7 +19886,7 @@ "starcitizentools/short-description": ">=4,<4.0.1", "starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1", "starcitizenwiki/embedvideo": "<=4", - "statamic/cms": "<=5.22", + "statamic/cms": "<5.73.6|>=6,<6.2.5", "stormpath/sdk": "<9.9.99", "studio-42/elfinder": "<=2.1.64", "studiomitte/friendlycaptcha": "<0.1.4", @@ -20094,7 +20025,7 @@ "vertexvaar/falsftp": "<0.2.6", "villagedefrance/opencart-overclocked": "<=1.11.1", "vova07/yii2-fileapi-widget": "<0.1.9", - "vrana/adminer": "<=4.8.1", + "vrana/adminer": "<5.4.2", "vufind/vufind": ">=2,<9.1.1", "waldhacker/hcaptcha": "<2.1.2", "wallabag/tcpdf": "<6.2.22", @@ -20224,7 +20155,7 @@ "type": "tidelift" } ], - "time": "2026-02-05T22:08:29+00:00" + "time": "2026-02-13T23:11:21+00:00" }, { "name": "sebastian/cli-parser", @@ -21414,16 +21345,16 @@ }, { "name": "symfony/maker-bundle", - "version": "v1.65.1", + "version": "v1.66.0", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "eba30452d212769c9a5bcf0716959fd8ba1e54e3" + "reference": "b5b4afa2a570b926682e9f34615a6766dd560ff4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/eba30452d212769c9a5bcf0716959fd8ba1e54e3", - "reference": "eba30452d212769c9a5bcf0716959fd8ba1e54e3", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/b5b4afa2a570b926682e9f34615a6766dd560ff4", + "reference": "b5b4afa2a570b926682e9f34615a6766dd560ff4", "shasum": "" }, "require": { @@ -21446,7 +21377,7 @@ }, "require-dev": { "composer/semver": "^3.0", - "doctrine/doctrine-bundle": "^2.5.0|^3.0.0", + "doctrine/doctrine-bundle": "^2.10|^3.0", "doctrine/orm": "^2.15|^3", "doctrine/persistence": "^3.1|^4.0", "symfony/http-client": "^6.4|^7.0|^8.0", @@ -21488,7 +21419,7 @@ ], "support": { "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.65.1" + "source": "https://github.com/symfony/maker-bundle/tree/v1.66.0" }, "funding": [ { @@ -21508,7 +21439,7 @@ "type": "tidelift" } ], - "time": "2025-12-02T07:14:37+00:00" + "time": "2026-02-09T08:55:54+00:00" }, { "name": "symfony/phpunit-bridge", diff --git a/config/packages/doctrine.php b/config/packages/doctrine.php index 47584ed7..e5be011f 100644 --- a/config/packages/doctrine.php +++ b/config/packages/doctrine.php @@ -20,12 +20,14 @@ declare(strict_types=1); +use Symfony\Config\DoctrineConfig; + /** * This class extends the default doctrine ORM configuration to enable native lazy objects on PHP 8.4+. * We have to do this in a PHP file, because the yaml file does not support conditionals on PHP version. */ -return static function(\Symfony\Config\DoctrineConfig $doctrine) { +return static function(DoctrineConfig $doctrine) { //On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies. if (PHP_VERSION_ID >= 80400) { $doctrine->orm()->enableNativeLazyObjects(true); diff --git a/config/permissions.yaml b/config/permissions.yaml index 0dabf9d3..39e91b57 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -68,6 +68,9 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co move: label: "perm.parts_stock.move" apiTokenRole: ROLE_API_EDIT + stocktake: + label: "perm.parts_stock.stocktake" + apiTokenRole: ROLE_API_EDIT storelocations: &PART_CONTAINING diff --git a/config/reference.php b/config/reference.php index a1a077aa..978a82f9 100644 --- a/config/reference.php +++ b/config/reference.php @@ -1387,7 +1387,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * bubble?: bool|Param, // Default: true * interactive_only?: bool|Param, // Default: false * app_name?: scalar|Param|null, // Default: null - * fill_extra_context?: bool|Param, // Default: false * include_stacktraces?: bool|Param, // Default: false * process_psr_3_messages?: array{ * enabled?: bool|Param|null, // Default: null @@ -1407,7 +1406,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * activation_strategy?: scalar|Param|null, // Default: null * stop_buffering?: bool|Param, // Default: true * passthru_level?: scalar|Param|null, // Default: null - * excluded_404s?: list, * excluded_http_codes?: list, @@ -1421,9 +1419,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * url?: scalar|Param|null, * exchange?: scalar|Param|null, * exchange_name?: scalar|Param|null, // Default: "log" - * room?: scalar|Param|null, - * message_format?: scalar|Param|null, // Default: "text" - * api_version?: scalar|Param|null, // Default: null * channel?: scalar|Param|null, // Default: null * bot_name?: scalar|Param|null, // Default: "Monolog" * use_attachment?: scalar|Param|null, // Default: true @@ -1432,9 +1427,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * icon_emoji?: scalar|Param|null, // Default: null * webhook_url?: scalar|Param|null, * exclude_fields?: list, - * team?: scalar|Param|null, - * notify?: scalar|Param|null, // Default: false - * nickname?: scalar|Param|null, // Default: "Monolog" * token?: scalar|Param|null, * region?: scalar|Param|null, * source?: scalar|Param|null, @@ -1452,12 +1444,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * store?: scalar|Param|null, // Default: null * connection_timeout?: scalar|Param|null, * persistent?: bool|Param, - * dsn?: scalar|Param|null, - * hub_id?: scalar|Param|null, // Default: null - * client_id?: scalar|Param|null, // Default: null - * auto_log_stacks?: scalar|Param|null, // Default: false - * release?: scalar|Param|null, // Default: null - * environment?: scalar|Param|null, // Default: null * message_type?: scalar|Param|null, // Default: 0 * parse_mode?: scalar|Param|null, // Default: null * disable_webpage_preview?: bool|Param|null, // Default: null @@ -1467,7 +1453,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * topic?: int|Param, // Default: null * factor?: int|Param, // Default: 1 * tags?: list, - * console_formater_options?: mixed, // Deprecated: "monolog.handlers..console_formater_options.console_formater_options" is deprecated, use "monolog.handlers..console_formater_options.console_formatter_options" instead. * console_formatter_options?: mixed, // Default: [] * formatter?: scalar|Param|null, * nested?: bool|Param, // Default: false @@ -1478,15 +1463,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * chunk_size?: scalar|Param|null, // Default: 1420 * encoder?: "json"|"compressed_json"|Param, * }, - * mongo?: string|array{ - * id?: scalar|Param|null, - * host?: scalar|Param|null, - * port?: scalar|Param|null, // Default: 27017 - * user?: scalar|Param|null, - * pass?: scalar|Param|null, - * database?: scalar|Param|null, // Default: "monolog" - * collection?: scalar|Param|null, // Default: "logs" - * }, * mongodb?: string|array{ * id?: scalar|Param|null, // ID of a MongoDB\Client service * uri?: scalar|Param|null, @@ -1529,7 +1505,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * id: scalar|Param|null, * method?: scalar|Param|null, // Default: null * }, - * lazy?: bool|Param, // Default: true * verbosity_levels?: array{ * VERBOSITY_QUIET?: scalar|Param|null, // Default: "ERROR" * VERBOSITY_NORMAL?: scalar|Param|null, // Default: "WARNING" diff --git a/docs/usage/labels.md b/docs/usage/labels.md index 4c3f8b32..c804cebb 100644 --- a/docs/usage/labels.md +++ b/docs/usage/labels.md @@ -91,18 +91,20 @@ in [official documentation](https://twig.symfony.com/doc/3.x/). Twig allows you for much more complex and dynamic label generation. You can use loops, conditions, and functions to create the label content and you can access almost all data Part-DB offers. The label templates are evaluated in a special sandboxed environment, -where only certain operations are allowed. Only read access to entities is allowed. However as it circumvents Part-DB normal permission system, +where only certain operations are allowed. Only read access to entities is allowed. However, as it circumvents Part-DB normal permission system, the twig mode is only available to users with the "Twig mode" permission. +It is useful to use the HTML embed feature of the editor, to have a block where you can write the twig code without worrying about the WYSIWYG editor messing with your code. + The following variables are in injected into Twig and can be accessed using `{% raw %}{{ variable }}{% endraw %}` ( or `{% raw %}{{ variable.property }}{% endraw %}`): | Variable name | Description | |--------------------------------------------|--------------------------------------------------------------------------------------| -| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog. | +| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog. | | `{% raw %}{{ user }}{% endraw %}` | The current logged in user. Null if you are not logged in | | `{% raw %}{{ install_title }}{% endraw %}` | The name of the current Part-DB instance (similar to [[INSTALL_NAME]] placeholder). | -| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated | +| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated ) | | `{% raw %}{{ last_page }}{% endraw %}` | The page number of the last element. Equals the number of all pages / element labels | | `{% raw %}{{ paper_width }}{% endraw %}` | The width of the label paper in mm | | `{% raw %}{{ paper_height }}{% endraw %}` | The height of the label paper in mm | @@ -236,12 +238,18 @@ certain data: #### Functions -| Function name | Description | -|----------------------------------------------|-----------------------------------------------------------------------------------------------| -| `placeholder(placeholder, element)` | Get the value of a placeholder of an element | -| `entity_type(element)` | Get the type of an entity as string | -| `entity_url(element, type)` | Get the URL to a specific entity type page (e.g. `info`, `edit`, etc.) | -| `barcode_svg(content, type)` | Generate a barcode SVG from the content and type (e.g. `QRCODE`, `CODE128` etc.). A svg string is returned, which you need to data uri encode to inline it. | +| Function name | Description | +|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `placeholder(placeholder, element)` | Get the value of a placeholder of an element | +| `entity_type(element)` | Get the type of an entity as string | +| `entity_url(element, type)` | Get the URL to a specific entity type page (e.g. `info`, `edit`, etc.) | +| `barcode_svg(content, type)` | Generate a barcode SVG from the content and type (e.g. `QRCODE`, `CODE128` etc.). A svg string is returned, which you need to data uri encode to inline it. | +| `associated_parts(element)` | Get the associated parts of an element like a storagelocation, footprint, etc. Only the directly associated parts are returned | +| `associated_parts_r(element)` | Get the associated parts of an element like a storagelocation, footprint, etc. including all sub-entities recursively (e.g. sub-locations) | +| `associated_parts_count(element)` | Get the count of associated parts of an element like a storagelocation, footprint, excluding sub-entities | +| `associated_parts_count_r(element)` | Get the count of associated parts of an element like a storagelocation, footprint, including all sub-entities recursively (e.g. sub-locations) | +| `type_label(element)` | Get the name of the type of an element (e.g. "Part", "Storage location", etc.) | +| `type_label_p(element)` | Get the name of the type of an element in plural form (e.g. "Parts", "Storage locations", etc.) | ### Filters @@ -285,5 +293,5 @@ If you want to use a different (more beautiful) font, you can use the [custom fo feature. There is the [Noto](https://www.google.com/get/noto/) font family from Google, which supports a lot of languages and is available in different styles (regular, bold, italic, bold-italic). -For example, you can use [Noto CJK](https://github.com/notofonts/noto-cjk) for more beautiful Chinese, Japanese, -and Korean characters. \ No newline at end of file +For example, you can use [Noto CJK](https://github.com/notofonts/noto-cjk) for more beautiful Chinese, Japanese, +and Korean characters. diff --git a/migrations/Version20260208131116.php b/migrations/Version20260208131116.php new file mode 100644 index 00000000..d05d3e4c --- /dev/null +++ b/migrations/Version20260208131116.php @@ -0,0 +1,129 @@ +addSql('ALTER TABLE attachment_types ADD allowed_targets LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE part_lots ADD last_stocktake_at DATETIME DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD gtin VARCHAR(255) DEFAULT NULL'); + $this->addSql('CREATE INDEX parts_idx_gtin ON parts (gtin)'); + $this->addSql('ALTER TABLE orderdetails ADD prices_includes_vat TINYINT DEFAULT NULL'); + } + + public function mySQLDown(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE `attachment_types` DROP allowed_targets'); + $this->addSql('DROP INDEX parts_idx_gtin ON `parts`'); + $this->addSql('ALTER TABLE `parts` DROP gtin'); + $this->addSql('ALTER TABLE part_lots DROP last_stocktake_at'); + $this->addSql('ALTER TABLE `orderdetails` DROP prices_includes_vat'); + } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql('ALTER TABLE attachment_types ADD COLUMN allowed_targets CLOB DEFAULT NULL'); + $this->addSql('ALTER TABLE part_lots ADD COLUMN last_stocktake_at DATETIME DEFAULT NULL'); + $this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint FROM parts'); + $this->addSql('DROP TABLE parts'); + $this->addSql('CREATE TABLE parts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, id_part_custom_state INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, ipn VARCHAR(100) DEFAULT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(2048) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_value VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, gtin VARCHAR(255) DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES footprints (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES measurement_units (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES manufacturers (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES part_custom_states (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES orderdetails (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO parts (id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint) SELECT id, id_preview_attachment, id_category, id_footprint, id_part_unit, id_manufacturer, id_part_custom_state, order_orderdetails_id, built_project_id, datetime_added, name, last_modified, needs_review, tags, mass, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, ipn, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint FROM __temp__parts'); + $this->addSql('DROP TABLE __temp__parts'); + $this->addSql('CREATE INDEX parts_idx_name ON parts (name)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON parts (ipn)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON parts (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON parts (order_orderdetails_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn)'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON parts (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON parts (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON parts (id_category)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON parts (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON parts (id_manufacturer)'); + $this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON parts (id_part_custom_state)'); + $this->addSql('CREATE INDEX parts_idx_gtin ON parts (gtin)'); + $this->addSql('ALTER TABLE orderdetails ADD COLUMN prices_includes_vat BOOLEAN DEFAULT NULL'); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql('CREATE TEMPORARY TABLE __temp__attachment_types AS SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, filetype_filter, parent_id, id_preview_attachment FROM "attachment_types"'); + $this->addSql('DROP TABLE "attachment_types"'); + $this->addSql('CREATE TABLE "attachment_types" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, comment CLOB NOT NULL, not_selectable BOOLEAN NOT NULL, alternative_names CLOB DEFAULT NULL, filetype_filter CLOB NOT NULL, parent_id INTEGER DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, CONSTRAINT FK_EFAED719727ACA70 FOREIGN KEY (parent_id) REFERENCES "attachment_types" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EFAED719EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "attachment_types" (id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, filetype_filter, parent_id, id_preview_attachment) SELECT id, name, last_modified, datetime_added, comment, not_selectable, alternative_names, filetype_filter, parent_id, id_preview_attachment FROM __temp__attachment_types'); + $this->addSql('DROP TABLE __temp__attachment_types'); + $this->addSql('CREATE INDEX IDX_EFAED719727ACA70 ON "attachment_types" (parent_id)'); + $this->addSql('CREATE INDEX IDX_EFAED719EA7100A1 ON "attachment_types" (id_preview_attachment)'); + $this->addSql('CREATE INDEX attachment_types_idx_name ON "attachment_types" (name)'); + $this->addSql('CREATE INDEX attachment_types_idx_parent_name ON "attachment_types" (parent_id, name)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__part_lots AS SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_modified, datetime_added, id_store_location, id_part, id_owner FROM part_lots'); + $this->addSql('DROP TABLE part_lots'); + $this->addSql('CREATE TABLE part_lots (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, expiration_date DATETIME DEFAULT NULL, instock_unknown BOOLEAN NOT NULL, amount DOUBLE PRECISION NOT NULL, needs_refill BOOLEAN NOT NULL, vendor_barcode VARCHAR(255) DEFAULT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, id_store_location INTEGER DEFAULT NULL, id_part INTEGER NOT NULL, id_owner INTEGER DEFAULT NULL, CONSTRAINT FK_EBC8F9435D8F4B37 FOREIGN KEY (id_store_location) REFERENCES "storelocations" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F943C22F6CC4 FOREIGN KEY (id_part) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_EBC8F94321E5A74C FOREIGN KEY (id_owner) REFERENCES "users" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO part_lots (id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_modified, datetime_added, id_store_location, id_part, id_owner) SELECT id, description, comment, expiration_date, instock_unknown, amount, needs_refill, vendor_barcode, last_modified, datetime_added, id_store_location, id_part, id_owner FROM __temp__part_lots'); + $this->addSql('DROP TABLE __temp__part_lots'); + $this->addSql('CREATE INDEX IDX_EBC8F9435D8F4B37 ON part_lots (id_store_location)'); + $this->addSql('CREATE INDEX IDX_EBC8F943C22F6CC4 ON part_lots (id_part)'); + $this->addSql('CREATE INDEX IDX_EBC8F94321E5A74C ON part_lots (id_owner)'); + $this->addSql('CREATE INDEX part_lots_idx_instock_un_expiration_id_part ON part_lots (instock_unknown, expiration_date, id_part)'); + $this->addSql('CREATE INDEX part_lots_idx_needs_refill ON part_lots (needs_refill)'); + $this->addSql('CREATE INDEX part_lots_idx_barcode ON part_lots (vendor_barcode)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__parts AS SELECT id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id FROM "parts"'); + $this->addSql('DROP TABLE "parts"'); + $this->addSql('CREATE TABLE "parts" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, needs_review BOOLEAN NOT NULL, tags CLOB NOT NULL, mass DOUBLE PRECISION DEFAULT NULL, ipn VARCHAR(100) DEFAULT NULL, description CLOB NOT NULL, comment CLOB NOT NULL, visible BOOLEAN NOT NULL, favorite BOOLEAN NOT NULL, minamount DOUBLE PRECISION NOT NULL, manufacturer_product_url CLOB NOT NULL, manufacturer_product_number VARCHAR(255) NOT NULL, manufacturing_status VARCHAR(255) DEFAULT NULL, order_quantity INTEGER NOT NULL, manual_order BOOLEAN NOT NULL, provider_reference_provider_key VARCHAR(255) DEFAULT NULL, provider_reference_provider_id VARCHAR(255) DEFAULT NULL, provider_reference_provider_url VARCHAR(2048) DEFAULT NULL, provider_reference_last_updated DATETIME DEFAULT NULL, eda_info_reference_prefix VARCHAR(255) DEFAULT NULL, eda_info_value VARCHAR(255) DEFAULT NULL, eda_info_invisible BOOLEAN DEFAULT NULL, eda_info_exclude_from_bom BOOLEAN DEFAULT NULL, eda_info_exclude_from_board BOOLEAN DEFAULT NULL, eda_info_exclude_from_sim BOOLEAN DEFAULT NULL, eda_info_kicad_symbol VARCHAR(255) DEFAULT NULL, eda_info_kicad_footprint VARCHAR(255) DEFAULT NULL, id_preview_attachment INTEGER DEFAULT NULL, id_part_custom_state INTEGER DEFAULT NULL, id_category INTEGER NOT NULL, id_footprint INTEGER DEFAULT NULL, id_part_unit INTEGER DEFAULT NULL, id_manufacturer INTEGER DEFAULT NULL, order_orderdetails_id INTEGER DEFAULT NULL, built_project_id INTEGER DEFAULT NULL, CONSTRAINT FK_6940A7FEEA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES "attachments" (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEA3ED1215 FOREIGN KEY (id_part_custom_state) REFERENCES "part_custom_states" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE5697F554 FOREIGN KEY (id_category) REFERENCES "categories" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE7E371A10 FOREIGN KEY (id_footprint) REFERENCES "footprints" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE2626CEF9 FOREIGN KEY (id_part_unit) REFERENCES "measurement_units" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE1ECB93AE FOREIGN KEY (id_manufacturer) REFERENCES "manufacturers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FE81081E9B FOREIGN KEY (order_orderdetails_id) REFERENCES "orderdetails" (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES projects (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "parts" (id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id) SELECT id, name, last_modified, datetime_added, needs_review, tags, mass, ipn, description, comment, visible, favorite, minamount, manufacturer_product_url, manufacturer_product_number, manufacturing_status, order_quantity, manual_order, provider_reference_provider_key, provider_reference_provider_id, provider_reference_provider_url, provider_reference_last_updated, eda_info_reference_prefix, eda_info_value, eda_info_invisible, eda_info_exclude_from_bom, eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol, eda_info_kicad_footprint, id_preview_attachment, id_part_custom_state, id_category, id_footprint, id_part_unit, id_manufacturer, order_orderdetails_id, built_project_id FROM __temp__parts'); + $this->addSql('DROP TABLE __temp__parts'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn)'); + $this->addSql('CREATE INDEX IDX_6940A7FEEA7100A1 ON "parts" (id_preview_attachment)'); + $this->addSql('CREATE INDEX IDX_6940A7FEA3ED1215 ON "parts" (id_part_custom_state)'); + $this->addSql('CREATE INDEX IDX_6940A7FE5697F554 ON "parts" (id_category)'); + $this->addSql('CREATE INDEX IDX_6940A7FE7E371A10 ON "parts" (id_footprint)'); + $this->addSql('CREATE INDEX IDX_6940A7FE2626CEF9 ON "parts" (id_part_unit)'); + $this->addSql('CREATE INDEX IDX_6940A7FE1ECB93AE ON "parts" (id_manufacturer)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE81081E9B ON "parts" (order_orderdetails_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON "parts" (built_project_id)'); + $this->addSql('CREATE INDEX parts_idx_datet_name_last_id_needs ON "parts" (datetime_added, name, last_modified, id, needs_review)'); + $this->addSql('CREATE INDEX parts_idx_name ON "parts" (name)'); + $this->addSql('CREATE INDEX parts_idx_ipn ON "parts" (ipn)'); + $this->addSql('CREATE TEMPORARY TABLE __temp__orderdetails AS SELECT id, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added, part_id, id_supplier FROM "orderdetails"'); + $this->addSql('DROP TABLE "orderdetails"'); + $this->addSql('CREATE TABLE "orderdetails" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, supplierpartnr VARCHAR(255) NOT NULL, obsolete BOOLEAN NOT NULL, supplier_product_url CLOB NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, part_id INTEGER NOT NULL, id_supplier INTEGER DEFAULT NULL, CONSTRAINT FK_489AFCDC4CE34BEC FOREIGN KEY (part_id) REFERENCES "parts" (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_489AFCDCCBF180EB FOREIGN KEY (id_supplier) REFERENCES "suppliers" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('INSERT INTO "orderdetails" (id, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added, part_id, id_supplier) SELECT id, supplierpartnr, obsolete, supplier_product_url, last_modified, datetime_added, part_id, id_supplier FROM __temp__orderdetails'); + $this->addSql('DROP TABLE __temp__orderdetails'); + $this->addSql('CREATE INDEX IDX_489AFCDC4CE34BEC ON "orderdetails" (part_id)'); + $this->addSql('CREATE INDEX IDX_489AFCDCCBF180EB ON "orderdetails" (id_supplier)'); + $this->addSql('CREATE INDEX orderdetails_supplier_part_nr ON "orderdetails" (supplierpartnr)'); + } + + public function postgreSQLUp(Schema $schema): void + { + $this->addSql('ALTER TABLE attachment_types ADD allowed_targets TEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE part_lots ADD last_stocktake_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD gtin VARCHAR(255) DEFAULT NULL'); + $this->addSql('CREATE INDEX parts_idx_gtin ON parts (gtin)'); + $this->addSql('ALTER TABLE orderdetails ADD prices_includes_vat BOOLEAN DEFAULT NULL'); + } + + public function postgreSQLDown(Schema $schema): void + { + $this->addSql('ALTER TABLE "attachment_types" DROP allowed_targets'); + $this->addSql('ALTER TABLE part_lots DROP last_stocktake_at'); + $this->addSql('DROP INDEX parts_idx_gtin'); + $this->addSql('ALTER TABLE "parts" DROP gtin'); + $this->addSql('ALTER TABLE "orderdetails" DROP prices_includes_vat'); + } +} diff --git a/rector.php b/rector.php index 936b447e..5a77a882 100644 --- a/rector.php +++ b/rector.php @@ -18,7 +18,7 @@ use Rector\Symfony\Set\SymfonySetList; use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector; return RectorConfig::configure() - ->withComposerBased(phpunit: true) + ->withComposerBased(phpunit: true, symfony: true) ->withSymfonyContainerPhp(__DIR__ . '/tests/symfony-container.php') ->withSymfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml') @@ -36,8 +36,6 @@ return RectorConfig::configure() PHPUnitSetList::PHPUNIT_90, PHPUnitSetList::PHPUNIT_110, PHPUnitSetList::PHPUNIT_CODE_QUALITY, - - ]) ->withRules([ @@ -59,6 +57,9 @@ return RectorConfig::configure() PreferPHPUnitThisCallRector::class, //Do not replace 'GET' with class constant, LiteralGetToRequestClassConstantRector::class, + + //Do not move help text of commands to the command class, as we want to keep the help text in the command definition for better readability + \Rector\Symfony\Symfony73\Rector\Class_\CommandHelpToAttributeRector::class ]) //Do not apply rules to Symfony own files @@ -67,6 +68,7 @@ return RectorConfig::configure() __DIR__ . '/src/Kernel.php', __DIR__ . '/config/preload.php', __DIR__ . '/config/bundles.php', + __DIR__ . '/config/reference.php' ]) ; diff --git a/src/ApiResource/LabelGenerationRequest.php b/src/ApiResource/LabelGenerationRequest.php new file mode 100644 index 00000000..9b4462a0 --- /dev/null +++ b/src/ApiResource/LabelGenerationRequest.php @@ -0,0 +1,84 @@ +. + */ + +namespace App\ApiResource; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\OpenApi\Model\RequestBody; +use ApiPlatform\OpenApi\Model\Response; +use App\Entity\LabelSystem\LabelSupportedElement; +use App\State\LabelGenerationProcessor; +use App\Validator\Constraints\Misc\ValidRange; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * API Resource for generating PDF labels for parts, part lots, or storage locations. + * This endpoint allows generating labels using saved label profiles. + */ +#[ApiResource( + uriTemplate: '/labels/generate', + description: 'Generate PDF labels for parts, part lots, or storage locations using label profiles.', + operations: [ + new Post( + inputFormats: ['json' => ['application/json']], + outputFormats: [], + openapi: new Operation( + responses: [ + "200" => new Response(description: "PDF file containing the generated labels"), + ], + summary: 'Generate PDF labels', + description: 'Generate PDF labels for one or more elements using a label profile. Returns a PDF file.', + requestBody: new RequestBody( + description: 'Label generation request', + required: true, + ), + ), + ) + ], + processor: LabelGenerationProcessor::class, +)] +class LabelGenerationRequest +{ + /** + * @var int The ID of the label profile to use for generation + */ + #[Assert\NotBlank(message: 'Profile ID is required')] + #[Assert\Positive(message: 'Profile ID must be a positive integer')] + public int $profileId = 0; + + /** + * @var string Comma-separated list of element IDs or ranges (e.g., "1,2,5-10,15") + */ + #[Assert\NotBlank(message: 'Element IDs are required')] + #[ValidRange()] + #[ApiProperty(example: "1,2,5-10,15")] + public string $elementIds = ''; + + /** + * @var LabelSupportedElement|null Optional: Override the element type. If not provided, uses profile's default. + */ + public ?LabelSupportedElement $elementType = null; +} diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index ef2bae5f..b4f46a27 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Controller; +use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob; use App\DataTables\LogDataTable; use App\Entity\Attachments\AttachmentUpload; use App\Entity\Parts\Category; @@ -54,12 +55,14 @@ use Exception; use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid; use Symfony\Contracts\Translation\TranslatorInterface; use function Symfony\Component\Translation\t; @@ -149,7 +152,7 @@ final class PartController extends AbstractController $jobId = $request->query->get('jobId'); $bulkJob = null; if ($jobId) { - $bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId); + $bulkJob = $this->em->getRepository(BulkInfoProviderImportJob::class)->find($jobId); // Verify user owns this job if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) { $bulkJob = null; @@ -170,7 +173,7 @@ final class PartController extends AbstractController throw $this->createAccessDeniedException('Invalid CSRF token'); } - $bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId); + $bulkJob = $this->em->getRepository(BulkInfoProviderImportJob::class)->find($jobId); if (!$bulkJob || $bulkJob->getCreatedBy() !== $this->getUser()) { throw $this->createNotFoundException('Bulk import job not found'); } @@ -336,7 +339,7 @@ final class PartController extends AbstractController $jobId = $request->query->get('jobId'); $bulkJob = null; if ($jobId) { - $bulkJob = $this->em->getRepository(\App\Entity\InfoProviderSystem\BulkInfoProviderImportJob::class)->find($jobId); + $bulkJob = $this->em->getRepository(BulkInfoProviderImportJob::class)->find($jobId); // Verify user owns this job if ($bulkJob && $bulkJob->getCreatedBy() !== $this->getUser()) { $bulkJob = null; @@ -463,6 +466,54 @@ final class PartController extends AbstractController ); } + #[Route(path: '/{id}/stocktake', name: 'part_stocktake', methods: ['POST'])] + #[IsCsrfTokenValid(new Expression("'part_stocktake-' ~ args['part'].getid()"), '_token')] + public function stocktakeHandler(Part $part, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper, + Request $request, + ): Response + { + $partLot = $em->find(PartLot::class, $request->request->get('lot_id')); + + //Check that the user is allowed to stocktake the partlot + $this->denyAccessUnlessGranted('stocktake', $partLot); + + if (!$partLot instanceof PartLot) { + throw new \RuntimeException('Part lot not found!'); + } + //Ensure that the partlot belongs to the part + if ($partLot->getPart() !== $part) { + throw new \RuntimeException("The origin partlot does not belong to the part!"); + } + + $actualAmount = (float) $request->request->get('actual_amount'); + $comment = $request->request->get('comment'); + + $timestamp = null; + $timestamp_str = $request->request->getString('timestamp', ''); + //Try to parse the timestamp + if ($timestamp_str !== '') { + $timestamp = new DateTime($timestamp_str); + } + + $withdrawAddHelper->stocktake($partLot, $actualAmount, $comment, $timestamp); + + //Ensure that the timestamp is not in the future + if ($timestamp !== null && $timestamp > new DateTime("+20min")) { + throw new \LogicException("The timestamp must not be in the future!"); + } + + //Save the changes to the DB + $em->flush(); + $this->addFlash('success', 'part.withdraw.success'); + + //If a redirect was passed, then redirect there + if ($request->request->get('_redirect')) { + return $this->redirect($request->request->get('_redirect')); + } + //Otherwise just redirect to the part page + return $this->redirectToRoute('part_info', ['id' => $part->getID()]); + } + #[Route(path: '/{id}/add_withdraw', name: 'part_add_withdraw', methods: ['POST'])] public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response { diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 4e2077c4..ad4d272f 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -147,10 +147,7 @@ class SecurityController extends AbstractController 'label' => 'user.settings.pw_confirm.label', ], 'invalid_message' => 'password_must_match', - 'constraints' => [new Length([ - 'min' => 6, - 'max' => 128, - ])], + 'constraints' => [new Length(min: 6, max: 128)], ]); $builder->add('submit', SubmitType::class, [ diff --git a/src/Controller/UserSettingsController.php b/src/Controller/UserSettingsController.php index 4e56015a..3f54e99c 100644 --- a/src/Controller/UserSettingsController.php +++ b/src/Controller/UserSettingsController.php @@ -295,10 +295,7 @@ class UserSettingsController extends AbstractController 'autocomplete' => 'new-password', ], ], - 'constraints' => [new Length([ - 'min' => 6, - 'max' => 128, - ])], + 'constraints' => [new Length(min: 6, max: 128)], ]) ->add('submit', SubmitType::class, [ 'label' => 'save', diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php index cf185dfd..a08293ca 100644 --- a/src/DataTables/Filters/PartFilter.php +++ b/src/DataTables/Filters/PartFilter.php @@ -66,6 +66,7 @@ class PartFilter implements FilterInterface public readonly BooleanConstraint $favorite; public readonly BooleanConstraint $needsReview; public readonly NumberConstraint $mass; + public readonly TextConstraint $gtin; public readonly DateTimeConstraint $lastModified; public readonly DateTimeConstraint $addedDate; public readonly EntityConstraint $category; @@ -132,6 +133,7 @@ class PartFilter implements FilterInterface $this->measurementUnit = new EntityConstraint($nodesListBuilder, MeasurementUnit::class, 'part.partUnit'); $this->partCustomState = new EntityConstraint($nodesListBuilder, PartCustomState::class, 'part.partCustomState'); $this->mass = new NumberConstraint('part.mass'); + $this->gtin = new TextConstraint('part.gtin'); $this->dbId = new IntConstraint('part.id'); $this->ipn = new TextConstraint('part.ipn'); $this->addedDate = new DateTimeConstraint('part.addedDate'); diff --git a/src/DataTables/Filters/PartSearchFilter.php b/src/DataTables/Filters/PartSearchFilter.php index 82f62e33..a5af3759 100644 --- a/src/DataTables/Filters/PartSearchFilter.php +++ b/src/DataTables/Filters/PartSearchFilter.php @@ -138,13 +138,21 @@ class PartSearchFilter implements FilterInterface if (($fields_to_search === [] && !$search_dbId) || $this->keyword === '') { return; } + + //Use equal expression to just search for exact numeric matches + if ($search_dbId) { + $expressions[] = $queryBuilder->expr()->eq('part.id', ':id_exact'); + $queryBuilder->setParameter('id_exact', (int) $this->keyword, + ParameterType::INTEGER); + return; + } if($this->regex) { //Convert the fields to search to a list of expressions $expressions = array_map(function (string $field): string { return sprintf("REGEXP(%s, :search_query) = TRUE", $field); }, $fields_to_search); - + //Add Or concatenation of the expressions to our query $queryBuilder->andWhere( $queryBuilder->expr()->orX(...$expressions) @@ -160,8 +168,6 @@ class PartSearchFilter implements FilterInterface //Split keyword on spaces, but limit token count $tokens = explode(' ', $this->keyword, 5); - $params = new \Doctrine\Common\Collections\ArrayCollection(); - //Perform search of every single token in every selected field //AND-combine the results (all tokens must be present in any result, but the order does not matter) for ($i = 0; $i < sizeof($tokens); $i++) { @@ -185,7 +191,7 @@ class PartSearchFilter implements FilterInterface $queryBuilder->expr()->orX(...$expressions) ); } - $queryBuilder->setParameters($params); + $queryBuilder->setParameters(new ArrayCollection($params)); } } diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index 0baee630..d2faba76 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -47,6 +47,7 @@ use App\Services\EntityURLGenerator; use App\Services\Formatters\AmountFormatter; use App\Settings\BehaviorSettings\TableSettings; use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; use Omines\DataTablesBundle\Column\TextColumn; @@ -218,6 +219,10 @@ final class PartsDataTable implements DataTableTypeInterface 'label' => $this->translator->trans('part.table.mass'), 'unit' => 'g' ]) + ->add('gtin', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.gtin'), + 'orderField' => 'NATSORT(part.gtin)' + ]) ->add('tags', TagsColumn::class, [ 'label' => $this->translator->trans('part.table.tags'), ]) @@ -329,6 +334,7 @@ final class PartsDataTable implements DataTableTypeInterface ->addSelect('orderdetails') ->addSelect('attachments') ->addSelect('storelocations') + ->addSelect('projectBomEntries') ->from(Part::class, 'part') ->leftJoin('part.category', 'category') ->leftJoin('part.master_picture_attachment', 'master_picture_attachment') @@ -343,6 +349,7 @@ final class PartsDataTable implements DataTableTypeInterface ->leftJoin('part.partUnit', 'partUnit') ->leftJoin('part.partCustomState', 'partCustomState') ->leftJoin('part.parameters', 'parameters') + ->leftJoin('part.project_bom_entries', 'projectBomEntries') ->where('part.id IN (:ids)') ->setParameter('ids', $ids) @@ -360,7 +367,12 @@ final class PartsDataTable implements DataTableTypeInterface ->addGroupBy('attachments') ->addGroupBy('partUnit') ->addGroupBy('partCustomState') - ->addGroupBy('parameters'); + ->addGroupBy('parameters') + ->addGroupBy('projectBomEntries') + + ->setHint(Query::HINT_READ_ONLY, true) + ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, false) + ; //Get the results in the same order as the IDs were passed FieldHelper::addOrderByFieldParam($builder, 'part.id', 'ids'); diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 259785cb..d4b15ac7 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -97,7 +97,7 @@ use function in_array; #[DiscriminatorMap(typeProperty: '_type', mapping: self::API_DISCRIMINATOR_MAP)] abstract class Attachment extends AbstractNamedDBElement { - private const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'PartCustomState' => PartCustomStateAttachment::class, 'Device' => ProjectAttachment::class, + final public const ORM_DISCRIMINATOR_MAP = ['Part' => PartAttachment::class, 'PartCustomState' => PartCustomStateAttachment::class, 'Device' => ProjectAttachment::class, 'AttachmentType' => AttachmentTypeAttachment::class, 'Category' => CategoryAttachment::class, 'Footprint' => FootprintAttachment::class, 'Manufacturer' => ManufacturerAttachment::class, 'Currency' => CurrencyAttachment::class, 'Group' => GroupAttachment::class, 'MeasurementUnit' => MeasurementUnitAttachment::class, @@ -136,7 +136,7 @@ abstract class Attachment extends AbstractNamedDBElement * @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses. * @phpstan-var class-string */ - protected const ALLOWED_ELEMENT_CLASS = AttachmentContainingDBElement::class; + public const ALLOWED_ELEMENT_CLASS = AttachmentContainingDBElement::class; /** * @var AttachmentUpload|null The options used for uploading a file to this attachment or modify it. diff --git a/src/Entity/Attachments/AttachmentType.php b/src/Entity/Attachments/AttachmentType.php index 22333c16..7a314ffe 100644 --- a/src/Entity/Attachments/AttachmentType.php +++ b/src/Entity/Attachments/AttachmentType.php @@ -134,6 +134,17 @@ class AttachmentType extends AbstractStructuralDBElement #[ORM\OneToMany(mappedBy: 'attachment_type', targetEntity: Attachment::class)] protected Collection $attachments_with_type; + /** + * @var string[]|null A list of allowed targets where this attachment type can be assigned to, as a list of portable names + */ + #[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)] + protected ?array $allowed_targets = null; + + /** + * @var class-string[]|null + */ + protected ?array $allowed_targets_parsed_cache = null; + #[Groups(['attachment_type:read'])] protected ?\DateTimeImmutable $addedDate = null; #[Groups(['attachment_type:read'])] @@ -184,4 +195,81 @@ class AttachmentType extends AbstractStructuralDBElement return $this; } + + /** + * Returns a list of allowed targets as class names (e.g. PartAttachment::class), where this attachment type can be assigned to. If null, there are no restrictions. + * @return class-string[]|null + */ + public function getAllowedTargets(): ?array + { + //Use cached value if available + if ($this->allowed_targets_parsed_cache !== null) { + return $this->allowed_targets_parsed_cache; + } + + if (empty($this->allowed_targets)) { + return null; + } + + $tmp = []; + foreach ($this->allowed_targets as $target) { + if (isset(Attachment::ORM_DISCRIMINATOR_MAP[$target])) { + $tmp[] = Attachment::ORM_DISCRIMINATOR_MAP[$target]; + } + //Otherwise ignore the entry, as it is invalid + } + + //Cache the parsed value + $this->allowed_targets_parsed_cache = $tmp; + return $tmp; + } + + /** + * Sets the allowed targets for this attachment type. Allowed targets are specified as a list of class names (e.g. PartAttachment::class). If null is passed, there are no restrictions. + * @param class-string[]|null $allowed_targets + * @return $this + */ + public function setAllowedTargets(?array $allowed_targets): self + { + if ($allowed_targets === null) { + $this->allowed_targets = null; + } else { + $tmp = []; + foreach ($allowed_targets as $target) { + $discriminator = array_search($target, Attachment::ORM_DISCRIMINATOR_MAP, true); + if ($discriminator !== false) { + $tmp[] = $discriminator; + } else { + throw new \InvalidArgumentException("Invalid allowed target: $target. Allowed targets must be a class name of an Attachment subclass."); + } + } + $this->allowed_targets = $tmp; + } + + //Reset the cache + $this->allowed_targets_parsed_cache = null; + return $this; + } + + /** + * Checks if this attachment type is allowed for the given attachment target. + * @param Attachment|string $attachment + * @return bool + */ + public function isAllowedForTarget(Attachment|string $attachment): bool + { + //If no restrictions are set, allow all targets + if ($this->getAllowedTargets() === null) { + return true; + } + + //Iterate over all allowed targets and check if the attachment is an instance of any of them + foreach ($this->getAllowedTargets() as $allowed_target) { + if (is_a($attachment, $allowed_target, true)) { + return true; + } + } + + return false; + } } diff --git a/src/Entity/LabelSystem/LabelProfile.php b/src/Entity/LabelSystem/LabelProfile.php index d3616c34..236c07f7 100644 --- a/src/Entity/LabelSystem/LabelProfile.php +++ b/src/Entity/LabelSystem/LabelProfile.php @@ -41,6 +41,12 @@ declare(strict_types=1); namespace App\Entity\LabelSystem; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\OpenApi\Model\Operation; use Doctrine\Common\Collections\Criteria; use App\Entity\Attachments\Attachment; use App\Repository\LabelProfileRepository; @@ -58,6 +64,22 @@ use Symfony\Component\Validator\Constraints as Assert; /** * @extends AttachmentContainingDBElement */ +#[ApiResource( + operations: [ + new Get( + normalizationContext: ['groups' => ['label_profile:read', 'simple']], + security: "is_granted('read', object)", + openapi: new Operation(summary: 'Get a label profile by ID') + ), + new GetCollection( + normalizationContext: ['groups' => ['label_profile:read', 'simple']], + security: "is_granted('@labels.create_labels')", + openapi: new Operation(summary: 'List all available label profiles') + ), + ], + paginationEnabled: false, +)] +#[ApiFilter(SearchFilter::class, properties: ['options.supported_element' => 'exact', 'show_in_dropdown' => 'exact'])] #[UniqueEntity(['name', 'options.supported_element'])] #[ORM\Entity(repositoryClass: LabelProfileRepository::class)] #[ORM\EntityListeners([TreeCacheInvalidationListener::class])] @@ -80,20 +102,21 @@ class LabelProfile extends AttachmentContainingDBElement */ #[Assert\Valid] #[ORM\Embedded(class: 'LabelOptions')] - #[Groups(["extended", "full", "import"])] + #[Groups(["extended", "full", "import", "label_profile:read"])] protected LabelOptions $options; /** * @var string The comment info for this element */ #[ORM\Column(type: Types::TEXT)] + #[Groups(["extended", "full", "import", "label_profile:read"])] protected string $comment = ''; /** * @var bool determines, if this label profile should be shown in the dropdown quick menu */ #[ORM\Column(type: Types::BOOLEAN)] - #[Groups(["extended", "full", "import"])] + #[Groups(["extended", "full", "import", "label_profile:read"])] protected bool $show_in_dropdown = true; public function __construct() diff --git a/src/Entity/LogSystem/PartStockChangeType.php b/src/Entity/LogSystem/PartStockChangeType.php index f69fe95f..79e4c6da 100644 --- a/src/Entity/LogSystem/PartStockChangeType.php +++ b/src/Entity/LogSystem/PartStockChangeType.php @@ -28,6 +28,8 @@ enum PartStockChangeType: string case WITHDRAW = "withdraw"; case MOVE = "move"; + case STOCKTAKE = "stock_take"; + /** * Converts the type to a short representation usable in the extra field of the log entry. * @return string @@ -38,6 +40,7 @@ enum PartStockChangeType: string self::ADD => 'a', self::WITHDRAW => 'w', self::MOVE => 'm', + self::STOCKTAKE => 's', }; } @@ -52,6 +55,7 @@ enum PartStockChangeType: string 'a' => self::ADD, 'w' => self::WITHDRAW, 'm' => self::MOVE, + 's' => self::STOCKTAKE, default => throw new \InvalidArgumentException("Invalid short type: $value"), }; } diff --git a/src/Entity/LogSystem/PartStockChangedLogEntry.php b/src/Entity/LogSystem/PartStockChangedLogEntry.php index 1bac9e9f..a46f2ecf 100644 --- a/src/Entity/LogSystem/PartStockChangedLogEntry.php +++ b/src/Entity/LogSystem/PartStockChangedLogEntry.php @@ -122,6 +122,11 @@ class PartStockChangedLogEntry extends AbstractLogEntry return new self(PartStockChangeType::MOVE, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, $move_to_target, action_timestamp: $action_timestamp); } + public static function stocktake(PartLot $lot, float $old_stock, float $new_stock, float $new_total_part_instock, string $comment, ?\DateTimeInterface $action_timestamp = null): self + { + return new self(PartStockChangeType::STOCKTAKE, $lot, $old_stock, $new_stock, $new_total_part_instock, $comment, action_timestamp: $action_timestamp); + } + /** * Returns the instock change type of this entry * @return PartStockChangeType diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index d0a279e3..5ac81b60 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -80,6 +80,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ORM\Index(columns: ['datetime_added', 'name', 'last_modified', 'id', 'needs_review'], name: 'parts_idx_datet_name_last_id_needs')] #[ORM\Index(columns: ['name'], name: 'parts_idx_name')] #[ORM\Index(columns: ['ipn'], name: 'parts_idx_ipn')] +#[ORM\Index(columns: ['gtin'], name: 'parts_idx_gtin')] #[ApiResource( operations: [ new Get(normalizationContext: [ diff --git a/src/Entity/Parts/PartLot.php b/src/Entity/Parts/PartLot.php index d893e6de..53ecd3d5 100644 --- a/src/Entity/Parts/PartLot.php +++ b/src/Entity/Parts/PartLot.php @@ -171,6 +171,14 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named #[Length(max: 255)] protected ?string $user_barcode = null; + /** + * @var \DateTimeImmutable|null The date when the last stocktake was performed for this part lot. Set to null, if no stocktake was performed yet. + */ + #[Groups(['extended', 'full', 'import', 'part_lot:read', 'part_lot:write'])] + #[ORM\Column( type: Types::DATETIME_IMMUTABLE, nullable: true)] + #[Year2038BugWorkaround] + protected ?\DateTimeImmutable $last_stocktake_at = null; + public function __clone() { if ($this->id) { @@ -391,6 +399,26 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named return $this; } + /** + * Returns the date when the last stocktake was performed for this part lot. Returns null, if no stocktake was performed yet. + * @return \DateTimeImmutable|null + */ + public function getLastStocktakeAt(): ?\DateTimeImmutable + { + return $this->last_stocktake_at; + } + + /** + * Sets the date when the last stocktake was performed for this part lot. Set to null, if no stocktake was performed yet. + * @param \DateTimeImmutable|null $last_stocktake_at + * @return $this + */ + public function setLastStocktakeAt(?\DateTimeImmutable $last_stocktake_at): self + { + $this->last_stocktake_at = $last_stocktake_at; + return $this; + } + #[Assert\Callback] diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index 2cee7f1a..065469b5 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -24,6 +24,7 @@ namespace App\Entity\Parts\PartTraits; use App\Entity\Parts\InfoProviderReference; use App\Entity\Parts\PartCustomState; +use App\Validator\Constraints\ValidGTIN; use Doctrine\DBAL\Types\Types; use App\Entity\Parts\Part; use Doctrine\ORM\Mapping as ORM; @@ -84,6 +85,14 @@ trait AdvancedPropertyTrait #[ORM\JoinColumn(name: 'id_part_custom_state')] protected ?PartCustomState $partCustomState = null; + /** + * @var string|null The GTIN (Global Trade Item Number) of the part, for example a UPC or EAN code + */ + #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] + #[ORM\Column(type: Types::STRING, nullable: true)] + #[ValidGTIN] + protected ?string $gtin = null; + /** * Checks if this part is marked, for that it needs further review. */ @@ -211,4 +220,26 @@ trait AdvancedPropertyTrait return $this; } + + /** + * Gets the GTIN (Global Trade Item Number) of the part, for example a UPC or EAN code. + * Returns null if no GTIN is set. + */ + public function getGtin(): ?string + { + return $this->gtin; + } + + /** + * Sets the GTIN (Global Trade Item Number) of the part, for example a UPC or EAN code. + * + * @param string|null $gtin The new GTIN of the part + * + * @return $this + */ + public function setGtin(?string $gtin): self + { + $this->gtin = $gtin; + return $this; + } } diff --git a/src/Entity/PriceInformations/Orderdetail.php b/src/Entity/PriceInformations/Orderdetail.php index 8ed76a46..58f69598 100644 --- a/src/Entity/PriceInformations/Orderdetail.php +++ b/src/Entity/PriceInformations/Orderdetail.php @@ -52,6 +52,7 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\SerializedName; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints\Length; @@ -147,6 +148,13 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N #[ORM\JoinColumn(name: 'id_supplier')] protected ?Supplier $supplier = null; + /** + * @var bool|null Whether the prices includes VAT or not. Null means, that it is not specified, if the prices includes VAT or not. + */ + #[ORM\Column(type: Types::BOOLEAN, nullable: true)] + #[Groups(['extended', 'full', 'import', 'orderdetail:read', 'orderdetail:write'])] + protected ?bool $prices_includes_vat = null; + public function __construct() { $this->pricedetails = new ArrayCollection(); @@ -388,6 +396,28 @@ class Orderdetail extends AbstractDBElement implements TimeStampableInterface, N return $this; } + /** + * Checks if the prices of this orderdetail include VAT. Null means, that it is not specified, if the prices includes + * VAT or not. + * @return bool|null + */ + public function getPricesIncludesVAT(): ?bool + { + return $this->prices_includes_vat; + } + + /** + * Sets whether the prices of this orderdetail include VAT. + * @param bool|null $includesVat + * @return $this + */ + public function setPricesIncludesVAT(?bool $includesVat): self + { + $this->prices_includes_vat = $includesVat; + + return $this; + } + public function getName(): string { return $this->getSupplierPartNr(); diff --git a/src/Entity/PriceInformations/Pricedetail.php b/src/Entity/PriceInformations/Pricedetail.php index 86a7bcd5..553b07a3 100644 --- a/src/Entity/PriceInformations/Pricedetail.php +++ b/src/Entity/PriceInformations/Pricedetail.php @@ -121,6 +121,8 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface #[Groups(['pricedetail:read:standalone', 'pricedetail:write'])] protected ?Orderdetail $orderdetail = null; + + public function __construct() { $this->price = BigDecimal::zero()->toScale(self::PRICE_PRECISION); @@ -264,6 +266,15 @@ class Pricedetail extends AbstractDBElement implements TimeStampableInterface return $this->currency?->getIsoCode(); } + /** + * Returns whether the price includes VAT or not. Null means, that it is not specified, if the price includes VAT or not. + * @return bool|null + */ + public function getIncludesVat(): ?bool + { + return $this->orderdetail?->getPricesIncludesVAT(); + } + /******************************************************************************** * * Setters diff --git a/src/Entity/UserSystem/PermissionData.php b/src/Entity/UserSystem/PermissionData.php index 9ebdc9c9..b7d1ff8f 100644 --- a/src/Entity/UserSystem/PermissionData.php +++ b/src/Entity/UserSystem/PermissionData.php @@ -43,7 +43,7 @@ final class PermissionData implements \JsonSerializable /** * The current schema version of the permission data */ - public const CURRENT_SCHEMA_VERSION = 3; + public const CURRENT_SCHEMA_VERSION = 4; /** * Creates a new Permission Data Instance using the given data. diff --git a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php index ecc25b4f..690448a5 100644 --- a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php +++ b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php @@ -1,5 +1,7 @@ getMessage(), 0, $previous); + parent::__construct($previous?->getMessage() ?? "Unknown message", 0, $previous); } /** diff --git a/src/Form/AdminPages/AttachmentTypeAdminForm.php b/src/Form/AdminPages/AttachmentTypeAdminForm.php index d777d4d4..7f9e7646 100644 --- a/src/Form/AdminPages/AttachmentTypeAdminForm.php +++ b/src/Form/AdminPages/AttachmentTypeAdminForm.php @@ -22,17 +22,23 @@ declare(strict_types=1); namespace App\Form\AdminPages; +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\PartAttachment; +use App\Entity\Attachments\ProjectAttachment; +use App\Services\ElementTypeNameGenerator; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Base\AbstractNamedDBElement; use App\Services\Attachments\FileTypeFilterTools; use App\Services\LogSystem\EventCommentNeededHelper; use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Translation\StaticMessage; class AttachmentTypeAdminForm extends BaseEntityAdminForm { - public function __construct(Security $security, protected FileTypeFilterTools $filterTools, EventCommentNeededHelper $eventCommentNeededHelper) + public function __construct(Security $security, protected FileTypeFilterTools $filterTools, EventCommentNeededHelper $eventCommentNeededHelper, private readonly ElementTypeNameGenerator $elementTypeNameGenerator) { parent::__construct($security, $eventCommentNeededHelper); } @@ -41,6 +47,25 @@ class AttachmentTypeAdminForm extends BaseEntityAdminForm { $is_new = null === $entity->getID(); + + $choiceLabel = function (string $class) { + if (!is_a($class, Attachment::class, true)) { + return $class; + } + return new StaticMessage($this->elementTypeNameGenerator->typeLabelPlural($class::ALLOWED_ELEMENT_CLASS)); + }; + + + $builder->add('allowed_targets', ChoiceType::class, [ + 'required' => false, + 'choices' => array_values(Attachment::ORM_DISCRIMINATOR_MAP), + 'choice_label' => $choiceLabel, + 'preferred_choices' => [PartAttachment::class, ProjectAttachment::class], + 'label' => 'attachment_type.edit.allowed_targets', + 'help' => 'attachment_type.edit.allowed_targets.help', + 'multiple' => true, + ]); + $builder->add('filetype_filter', TextType::class, [ 'required' => false, 'label' => 'attachment_type.edit.filetype_filter', diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index 5a4ef5bc..f4bf37f8 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -71,6 +71,7 @@ class BaseEntityAdminForm extends AbstractType 'label' => 'name.label', 'attr' => [ 'placeholder' => 'part.name.placeholder', + 'autofocus' => $is_new, ], 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); diff --git a/src/Form/AttachmentFormType.php b/src/Form/AttachmentFormType.php index eb484a58..d9fe2cd2 100644 --- a/src/Form/AttachmentFormType.php +++ b/src/Form/AttachmentFormType.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Form; +use App\Form\Type\AttachmentTypeType; use App\Settings\SystemSettings\AttachmentsSettings; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Attachments\Attachment; @@ -67,10 +68,10 @@ class AttachmentFormType extends AbstractType 'required' => false, 'empty_data' => '', ]) - ->add('attachment_type', StructuralEntityType::class, [ + ->add('attachment_type', AttachmentTypeType::class, [ 'label' => 'attachment.edit.attachment_type', - 'class' => AttachmentType::class, 'disable_not_selectable' => true, + 'attachment_filter_class' => $options['data_class'] ?? null, 'allow_add' => $this->security->isGranted('@attachment_types.create'), ]); @@ -121,9 +122,7 @@ class AttachmentFormType extends AbstractType ], 'constraints' => [ //new AllowedFileExtension(), - new File([ - 'maxSize' => $options['max_file_size'], - ]), + new File(maxSize: $options['max_file_size']), ], ]); diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index e101c635..25fe70b2 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -135,6 +135,10 @@ class PartFilterType extends AbstractType 'min' => 0, ]); + $builder->add('gtin', TextConstraintType::class, [ + 'label' => 'part.gtin', + ]); + $builder->add('measurementUnit', StructuralEntityConstraintType::class, [ 'label' => 'part.edit.partUnit', 'entity_class' => MeasurementUnit::class diff --git a/src/Form/InfoProviderSystem/FieldToProviderMappingType.php b/src/Form/InfoProviderSystem/FieldToProviderMappingType.php index 13e9581e..7df8985e 100644 --- a/src/Form/InfoProviderSystem/FieldToProviderMappingType.php +++ b/src/Form/InfoProviderSystem/FieldToProviderMappingType.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Form\InfoProviderSystem; +use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; @@ -61,7 +62,7 @@ class FieldToProviderMappingType extends AbstractType 'style' => 'width: 80px;' ], 'constraints' => [ - new \Symfony\Component\Validator\Constraints\Range(['min' => 1, 'max' => 10]), + new Range(min: 1, max: 10), ], ]); } diff --git a/src/Form/LabelSystem/ScanDialogType.php b/src/Form/LabelSystem/ScanDialogType.php index 13ff8e6f..9199c31d 100644 --- a/src/Form/LabelSystem/ScanDialogType.php +++ b/src/Form/LabelSystem/ScanDialogType.php @@ -75,7 +75,8 @@ class ScanDialogType extends AbstractType BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal', BarcodeSourceType::IPN => 'scan_dialog.mode.ipn', BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user', - BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp' + BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp', + BarcodeSourceType::GTIN => 'scan_dialog.mode.gtin', }, ]); diff --git a/src/Form/Part/OrderdetailType.php b/src/Form/Part/OrderdetailType.php index 53240821..ca295c7e 100644 --- a/src/Form/Part/OrderdetailType.php +++ b/src/Form/Part/OrderdetailType.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Form\Part; +use App\Form\Type\TriStateCheckboxType; use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Supplier; @@ -73,6 +74,11 @@ class OrderdetailType extends AbstractType 'label' => 'orderdetails.edit.obsolete', ]); + $builder->add('pricesIncludesVAT', TriStateCheckboxType::class, [ + 'required' => false, + 'label' => 'orderdetails.edit.prices_includes_vat', + ]); + //Add pricedetails after we know the data, so we can set the default currency $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options): void { /** @var Orderdetail $orderdetail */ diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index b8276589..6b929486 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -43,6 +43,7 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\LogSystem\EventCommentNeededHelper; use App\Services\LogSystem\EventCommentType; use App\Settings\MiscSettings\IpnSuggestSettings; +use App\Settings\SystemSettings\LocalizationSettings; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; @@ -63,6 +64,7 @@ class PartBaseType extends AbstractType protected UrlGeneratorInterface $urlGenerator, protected EventCommentNeededHelper $event_comment_needed_helper, protected IpnSuggestSettings $ipnSuggestSettings, + private readonly LocalizationSettings $localizationSettings, ) { } @@ -115,6 +117,7 @@ class PartBaseType extends AbstractType 'label' => 'part.edit.name', 'attr' => [ 'placeholder' => 'part.edit.name.placeholder', + 'autofocus' => $new_part, ], ]) ->add('description', RichTextEditorType::class, [ @@ -216,7 +219,13 @@ class PartBaseType extends AbstractType 'disable_not_selectable' => true, 'label' => 'part.edit.partCustomState', ]) - ->add('ipn', TextType::class, $ipnOptions); + ->add('ipn', TextType::class, $ipnOptions) + ->add('gtin', TextType::class, [ + 'required' => false, + 'empty_data' => null, + 'label' => 'part.gtin', + ]) + ; //Comment section $builder->add('comment', RichTextEditorType::class, [ @@ -261,6 +270,9 @@ class PartBaseType extends AbstractType 'entity' => $part, ]); + $orderdetailPrototype = new Orderdetail(); + $orderdetailPrototype->setPricesIncludesVAT($this->localizationSettings->pricesIncludeTaxByDefault); + //Orderdetails section $builder->add('orderdetails', CollectionType::class, [ 'entry_type' => OrderdetailType::class, @@ -269,7 +281,7 @@ class PartBaseType extends AbstractType 'allow_delete' => true, 'label' => false, 'by_reference' => false, - 'prototype_data' => new Orderdetail(), + 'prototype_data' => $orderdetailPrototype, 'entry_options' => [ 'measurement_unit' => $part->getPartUnit(), ], diff --git a/src/Form/Part/PartLotType.php b/src/Form/Part/PartLotType.php index 7d545340..ae86fb61 100644 --- a/src/Form/Part/PartLotType.php +++ b/src/Form/Part/PartLotType.php @@ -31,6 +31,7 @@ use App\Form\Type\StructuralEntityType; use App\Form\Type\UserSelectType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -110,6 +111,14 @@ class PartLotType extends AbstractType //Do not remove whitespace chars on the beginning and end of the string 'trim' => false, ]); + + $builder->add('last_stocktake_at', DateTimeType::class, [ + 'label' => 'part_lot.edit.last_stocktake_at', + 'widget' => 'single_text', + 'disabled' => !$this->security->isGranted('@parts_stock.stocktake'), + 'required' => false, + 'empty_data' => null, + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/Type/AttachmentTypeType.php b/src/Form/Type/AttachmentTypeType.php new file mode 100644 index 00000000..099ed282 --- /dev/null +++ b/src/Form/Type/AttachmentTypeType.php @@ -0,0 +1,56 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Type; + +use App\Entity\Attachments\AttachmentType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Form type to select the AttachmentType to use in an attachment form. This is used to filter the available attachment types based on the target class. + */ +class AttachmentTypeType extends AbstractType +{ + public function getParent(): ?string + { + return StructuralEntityType::class; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->define('attachment_filter_class')->allowedTypes('null', 'string')->default(null); + + $resolver->setDefault('class', AttachmentType::class); + + $resolver->setDefault('choice_filter', function (Options $options) { + if (is_a($options['class'], AttachmentType::class, true) && $options['attachment_filter_class'] !== null) { + return static function (?AttachmentType $choice) use ($options) { + return $choice?->isAllowedForTarget($options['attachment_filter_class']); + }; + } + return null; + }); + } +} diff --git a/src/Form/UserAdminForm.php b/src/Form/UserAdminForm.php index 69be181f..457a6e0b 100644 --- a/src/Form/UserAdminForm.php +++ b/src/Form/UserAdminForm.php @@ -177,10 +177,7 @@ class UserAdminForm extends AbstractType 'required' => false, 'mapped' => false, 'disabled' => !$this->security->isGranted('set_password', $entity) || $entity->isSamlUser(), - 'constraints' => [new Length([ - 'min' => 6, - 'max' => 128, - ])], + 'constraints' => [new Length(min: 6, max: 128)], ]) ->add('need_pw_change', CheckboxType::class, [ diff --git a/src/Form/UserSettingsType.php b/src/Form/UserSettingsType.php index 0c7cb169..968d8063 100644 --- a/src/Form/UserSettingsType.php +++ b/src/Form/UserSettingsType.php @@ -92,9 +92,7 @@ class UserSettingsType extends AbstractType 'accept' => 'image/*', ], 'constraints' => [ - new File([ - 'maxSize' => '5M', - ]), + new File(maxSize: '5M'), ], ]) ->add('aboutMe', RichTextEditorType::class, [ diff --git a/src/Security/Voter/PartLotVoter.php b/src/Security/Voter/PartLotVoter.php index 87c3d135..5748f4af 100644 --- a/src/Security/Voter/PartLotVoter.php +++ b/src/Security/Voter/PartLotVoter.php @@ -58,13 +58,13 @@ final class PartLotVoter extends Voter { } - protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element', 'withdraw', 'add', 'move']; + protected const ALLOWED_PERMS = ['read', 'edit', 'create', 'delete', 'show_history', 'revert_element', 'withdraw', 'add', 'move', 'stocktake']; protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token, ?Vote $vote = null): bool { $user = $this->helper->resolveUser($token); - if (in_array($attribute, ['withdraw', 'add', 'move'], true)) + if (in_array($attribute, ['withdraw', 'add', 'move', 'stocktake'], true)) { $base_permission = $this->helper->isGranted($token, 'parts_stock', $attribute, $vote); diff --git a/src/Services/EntityMergers/Mergers/PartMerger.php b/src/Services/EntityMergers/Mergers/PartMerger.php index d1f5c137..8397257e 100644 --- a/src/Services/EntityMergers/Mergers/PartMerger.php +++ b/src/Services/EntityMergers/Mergers/PartMerger.php @@ -59,6 +59,7 @@ class PartMerger implements EntityMergerInterface $this->useOtherValueIfNotEmtpy($target, $other, 'manufacturer_product_number'); $this->useOtherValueIfNotEmtpy($target, $other, 'mass'); $this->useOtherValueIfNotEmtpy($target, $other, 'ipn'); + $this->useOtherValueIfNotEmtpy($target, $other, 'gtin'); //Merge relations to other entities $this->useOtherValueIfNotNull($target, $other, 'manufacturer'); @@ -184,4 +185,4 @@ class PartMerger implements EntityMergerInterface } } } -} \ No newline at end of file +} diff --git a/src/Services/ImportExportSystem/BOMImporter.php b/src/Services/ImportExportSystem/BOMImporter.php index 8a91c825..abf72d74 100644 --- a/src/Services/ImportExportSystem/BOMImporter.php +++ b/src/Services/ImportExportSystem/BOMImporter.php @@ -22,6 +22,8 @@ declare(strict_types=1); */ namespace App\Services\ImportExportSystem; +use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Orderdetail; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; @@ -275,7 +277,7 @@ class BOMImporter $mapped_entries = []; // Collect all mapped entries for validation // Fetch suppliers once for efficiency - $suppliers = $this->entityManager->getRepository(\App\Entity\Parts\Supplier::class)->findAll(); + $suppliers = $this->entityManager->getRepository(Supplier::class)->findAll(); $supplierSPNKeys = []; $suppliersByName = []; // Map supplier names to supplier objects foreach ($suppliers as $supplier) { @@ -371,7 +373,7 @@ class BOMImporter if ($supplier_spn !== null) { // Query for orderdetails with matching supplier and SPN - $orderdetail = $this->entityManager->getRepository(\App\Entity\PriceInformations\Orderdetail::class) + $orderdetail = $this->entityManager->getRepository(Orderdetail::class) ->findOneBy([ 'supplier' => $supplier, 'supplierpartnr' => $supplier_spn, @@ -535,7 +537,7 @@ class BOMImporter ]; // Add dynamic supplier fields based on available suppliers in the database - $suppliers = $this->entityManager->getRepository(\App\Entity\Parts\Supplier::class)->findAll(); + $suppliers = $this->entityManager->getRepository(Supplier::class)->findAll(); foreach ($suppliers as $supplier) { $supplierName = $supplier->getName(); $targets[$supplierName . ' SPN'] = [ @@ -570,7 +572,7 @@ class BOMImporter ]; // Add supplier-specific patterns - $suppliers = $this->entityManager->getRepository(\App\Entity\Parts\Supplier::class)->findAll(); + $suppliers = $this->entityManager->getRepository(Supplier::class)->findAll(); foreach ($suppliers as $supplier) { $supplierName = $supplier->getName(); $supplierLower = strtolower($supplierName); diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 70feb8e6..ab87a905 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Services\ImportExportSystem; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Helpers\FilenameSanatizer; @@ -177,7 +178,7 @@ class EntityExporter $colIndex = 1; foreach ($columns as $column) { - $cellCoordinate = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex) . $rowIndex; + $cellCoordinate = Coordinate::stringFromColumnIndex($colIndex) . $rowIndex; $worksheet->setCellValue($cellCoordinate, $column); $colIndex++; } @@ -265,11 +266,14 @@ class EntityExporter //Sanitize the filename $filename = FilenameSanatizer::sanitizeFilename($filename); + //Remove percent for fallback + $fallback = str_replace("%", "_", $filename); + // Create the disposition of the file $disposition = $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename, - u($filename)->ascii()->toString(), + $fallback, ); // Set the content disposition $response->headers->set('Content-Disposition', $disposition); diff --git a/src/Services/ImportExportSystem/EntityImporter.php b/src/Services/ImportExportSystem/EntityImporter.php index a89be9dc..7b928d6c 100644 --- a/src/Services/ImportExportSystem/EntityImporter.php +++ b/src/Services/ImportExportSystem/EntityImporter.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Services\ImportExportSystem; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Parts\Category; @@ -419,14 +420,14 @@ class EntityImporter 'worksheet_title' => $worksheet->getTitle() ]); - $highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); + $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); for ($row = 1; $row <= $highestRow; $row++) { $rowData = []; // Read all columns using numeric index for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) { - $col = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex); + $col = Coordinate::stringFromColumnIndex($colIndex); try { $cellValue = $worksheet->getCell("{$col}{$row}")->getCalculatedValue(); $rowData[] = $cellValue ?? ''; diff --git a/src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php b/src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php index 58e9e240..3db09de3 100644 --- a/src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\DTOs; +use Doctrine\ORM\Exception\ORMException; use App\Entity\Parts\Part; use Doctrine\ORM\EntityManagerInterface; use Traversable; @@ -176,7 +177,7 @@ readonly class BulkSearchResponseDTO implements \ArrayAccess, \IteratorAggregate * @param array $data * @param EntityManagerInterface $entityManager * @return BulkSearchResponseDTO - * @throws \Doctrine\ORM\Exception\ORMException + * @throws ORMException */ public static function fromSerializableRepresentation(array $data, EntityManagerInterface $entityManager): BulkSearchResponseDTO { diff --git a/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php index 41d50510..9700ae57 100644 --- a/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/PartDetailDTO.php @@ -42,6 +42,7 @@ class PartDetailDTO extends SearchResultDTO ?ManufacturingStatus $manufacturing_status = null, ?string $provider_url = null, ?string $footprint = null, + ?string $gtin = null, public readonly ?string $notes = null, /** @var FileDTO[]|null */ public readonly ?array $datasheets = null, @@ -68,6 +69,7 @@ class PartDetailDTO extends SearchResultDTO manufacturing_status: $manufacturing_status, provider_url: $provider_url, footprint: $footprint, + gtin: $gtin ); } } diff --git a/src/Services/InfoProviderSystem/DTOs/PriceDTO.php b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php index 2acf3e57..cf1f577d 100644 --- a/src/Services/InfoProviderSystem/DTOs/PriceDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/PriceDTO.php @@ -39,7 +39,9 @@ readonly class PriceDTO public string $price, /** @var string The currency of the used ISO code of this price detail */ public ?string $currency_iso_code, - /** @var bool If the price includes tax */ + /** @var bool If the price includes tax + * @deprecated Use the prices_include_vat property of the PurchaseInfoDTO instead, as this property is not reliable if there are multiple prices with different values for includes_tax + */ public ?bool $includes_tax = true, /** @var float the price related quantity */ public ?float $price_related_quantity = 1.0, diff --git a/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php b/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php index 9ac142ff..446d04dc 100644 --- a/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/PurchaseInfoDTO.php @@ -29,6 +29,9 @@ namespace App\Services\InfoProviderSystem\DTOs; */ readonly class PurchaseInfoDTO { + /** @var bool|null If the prices contain VAT or not. Null if state is unknown. */ + public ?bool $prices_include_vat; + public function __construct( public string $distributor_name, public string $order_number, @@ -36,6 +39,7 @@ readonly class PurchaseInfoDTO public array $prices, /** @var string|null An url to the product page of the vendor */ public ?string $product_url = null, + ?bool $prices_include_vat = null, ) { //Ensure that the prices are PriceDTO instances @@ -44,5 +48,17 @@ readonly class PurchaseInfoDTO throw new \InvalidArgumentException('The prices array must only contain PriceDTO instances'); } } + + //If no prices_include_vat information is given, try to deduct it from the prices + if ($prices_include_vat === null) { + $vatValues = array_unique(array_map(fn(PriceDTO $price) => $price->includes_tax, $this->prices)); + if (count($vatValues) === 1) { + $this->prices_include_vat = $vatValues[0]; //Use the value of the prices if they are all the same + } else { + $this->prices_include_vat = null; //If there are different values for the prices, we cannot determine if the prices include VAT or not + } + } else { + $this->prices_include_vat = $prices_include_vat; + } } } diff --git a/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php index a70b2486..085ae17e 100644 --- a/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/SearchResultDTO.php @@ -59,6 +59,8 @@ class SearchResultDTO public readonly ?string $provider_url = null, /** @var string|null A footprint representation of the providers page */ public readonly ?string $footprint = null, + /** @var string|null The GTIN / EAN of the part */ + public readonly ?string $gtin = null, ) { if ($preview_image_url !== null) { @@ -90,6 +92,7 @@ class SearchResultDTO 'manufacturing_status' => $this->manufacturing_status?->value, 'provider_url' => $this->provider_url, 'footprint' => $this->footprint, + 'gtin' => $this->gtin, ]; } @@ -112,6 +115,7 @@ class SearchResultDTO manufacturing_status: isset($data['manufacturing_status']) ? ManufacturingStatus::tryFrom($data['manufacturing_status']) : null, provider_url: $data['provider_url'] ?? null, footprint: $data['footprint'] ?? null, + gtin: $data['gtin'] ?? null, ); } } diff --git a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php index a655a0df..c7c15673 100644 --- a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php +++ b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php @@ -94,7 +94,6 @@ final class DTOtoEntityConverter $entity->setPrice($dto->getPriceAsBigDecimal()); $entity->setPriceRelatedQuantity($dto->price_related_quantity); - //Currency TODO if ($dto->currency_iso_code !== null) { $entity->setCurrency($this->getCurrency($dto->currency_iso_code)); } else { @@ -117,6 +116,8 @@ final class DTOtoEntityConverter $entity->addPricedetail($this->convertPrice($price)); } + $entity->setPricesIncludesVAT($dto->prices_include_vat); + return $entity; } @@ -175,6 +176,8 @@ final class DTOtoEntityConverter $entity->setManufacturingStatus($dto->manufacturing_status ?? ManufacturingStatus::NOT_SET); $entity->setManufacturerProductURL($dto->manufacturer_product_url ?? ''); + $entity->setGtin($dto->gtin); + //Set the provider reference on the part $entity->setProviderReference(InfoProviderReference::fromPartDTO($dto)); diff --git a/src/Services/InfoProviderSystem/PartInfoRetriever.php b/src/Services/InfoProviderSystem/PartInfoRetriever.php index 0eb74642..9a24f3ae 100644 --- a/src/Services/InfoProviderSystem/PartInfoRetriever.php +++ b/src/Services/InfoProviderSystem/PartInfoRetriever.php @@ -82,7 +82,7 @@ final class PartInfoRetriever protected function searchInProvider(InfoProviderInterface $provider, string $keyword): array { //Generate key and escape reserved characters from the provider id - $escaped_keyword = urlencode($keyword); + $escaped_keyword = hash('xxh3', $keyword); return $this->partInfoCache->get("search_{$provider->getProviderKey()}_{$escaped_keyword}", function (ItemInterface $item) use ($provider, $keyword) { //Set the expiration time $item->expiresAfter(!$this->debugMode ? self::CACHE_RESULT_EXPIRATION : 1); @@ -108,7 +108,7 @@ final class PartInfoRetriever } //Generate key and escape reserved characters from the provider id - $escaped_part_id = urlencode($part_id); + $escaped_part_id = hash('xxh3', $part_id); return $this->partInfoCache->get("details_{$provider_key}_{$escaped_part_id}", function (ItemInterface $item) use ($provider, $part_id) { //Set the expiration time $item->expiresAfter(!$this->debugMode ? self::CACHE_DETAIL_EXPIRATION : 1); @@ -145,4 +145,4 @@ final class PartInfoRetriever return $this->dto_to_entity_converter->convertPart($details); } -} \ No newline at end of file +} diff --git a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php index aa165bfe..c2291107 100644 --- a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php +++ b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php @@ -365,7 +365,7 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv * - prefers 'zoom' format, then 'product' format, then all others * * @param array|null $images - * @return \App\Services\InfoProviderSystem\DTOs\FileDTO[] + * @return FileDTO[] */ private function getProductImages(?array $images): array { diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index 32434dee..3086b7d8 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -120,6 +120,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr preview_image_url: $result['image'] ?? null, provider_url: $this->getProductUrl($result['productId']), footprint: $this->getFootprintFromTechnicalDetails($result['technicalDetails'] ?? []), + gtin: $result['ean'] ?? null, ); } @@ -302,6 +303,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr preview_image_url: $data['productShortInformation']['mainImage']['imageUrl'] ?? null, provider_url: $this->getProductUrl($data['shortProductNumber']), footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []), + gtin: $data['productFullInformation']['eanCode'] ?? null, notes: $data['productFullInformation']['description'] ?? null, datasheets: $this->productMediaToDatasheets($data['productMedia'] ?? []), parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []), @@ -316,6 +318,8 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr ProviderCapabilities::PICTURE, ProviderCapabilities::DATASHEET, ProviderCapabilities::PRICE, + ProviderCapabilities::FOOTPRINT, + ProviderCapabilities::GTIN, ]; } diff --git a/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php b/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php index 7fbf5a58..ada72ea2 100644 --- a/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php +++ b/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php @@ -227,10 +227,11 @@ class GenericWebProvider implements InfoProviderInterface mpn: $product->mpn?->toString(), preview_image_url: $image, provider_url: $url, + gtin: $product->gtin14?->toString() ?? $product->gtin13?->toString() ?? $product->gtin12?->toString() ?? $product->gtin8?->toString(), notes: $notes, parameters: $parameters, vendor_infos: $vendor_infos, - mass: $mass + mass: $mass, ); } @@ -429,7 +430,8 @@ class GenericWebProvider implements InfoProviderInterface return [ ProviderCapabilities::BASIC, ProviderCapabilities::PICTURE, - ProviderCapabilities::PRICE + ProviderCapabilities::PRICE, + ProviderCapabilities::GTIN, ]; } } diff --git a/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php index bced19de..21fba53b 100644 --- a/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php +++ b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php @@ -43,6 +43,9 @@ enum ProviderCapabilities /** Information about the footprint of a part */ case FOOTPRINT; + /** Provider can provide GTIN for a part */ + case GTIN; + /** * Get the order index for displaying capabilities in a stable order. * @return int @@ -55,6 +58,7 @@ enum ProviderCapabilities self::DATASHEET => 3, self::PRICE => 4, self::FOOTPRINT => 5, + self::GTIN => 6, }; } @@ -66,6 +70,7 @@ enum ProviderCapabilities self::PICTURE => 'picture', self::DATASHEET => 'datasheet', self::PRICE => 'price', + self::GTIN => 'gtin', }; } @@ -77,6 +82,7 @@ enum ProviderCapabilities self::PICTURE => 'fa-image', self::DATASHEET => 'fa-file-alt', self::PRICE => 'fa-money-bill-wave', + self::GTIN => 'fa-barcode', }; } } diff --git a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php index 5c8efbf1..88bf33cb 100644 --- a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php @@ -84,6 +84,8 @@ class ReicheltProvider implements InfoProviderInterface $name = $element->filter('meta[itemprop="name"]')->attr('content'); $sku = $element->filter('meta[itemprop="sku"]')->attr('content'); + + //Try to extract a picture URL: $pictureURL = $element->filter("div.al_artlogo img")->attr('src'); @@ -95,7 +97,8 @@ class ReicheltProvider implements InfoProviderInterface category: null, manufacturer: $sku, preview_image_url: $pictureURL, - provider_url: $element->filter('a.al_artinfo_link')->attr('href') + provider_url: $element->filter('a.al_artinfo_link')->attr('href'), + ); }); @@ -146,6 +149,15 @@ class ReicheltProvider implements InfoProviderInterface $priceString = $dom->filter('meta[itemprop="price"]')->attr('content'); $currency = $dom->filter('meta[itemprop="priceCurrency"]')->attr('content', 'EUR'); + $gtin = null; + foreach (['gtin13', 'gtin14', 'gtin12', 'gtin8'] as $gtinType) { + if ($dom->filter("[itemprop=\"$gtinType\"]")->count() > 0) { + $gtin = $dom->filter("[itemprop=\"$gtinType\"]")->innerText(); + break; + } + } + + //Create purchase info $purchaseInfo = new PurchaseInfoDTO( distributor_name: self::DISTRIBUTOR_NAME, @@ -167,10 +179,11 @@ class ReicheltProvider implements InfoProviderInterface mpn: $this->parseMPN($dom), preview_image_url: $json[0]['article_picture'], provider_url: $productPage, + gtin: $gtin, notes: $notes, datasheets: $datasheets, parameters: $this->parseParameters($dom), - vendor_infos: [$purchaseInfo] + vendor_infos: [$purchaseInfo], ); } @@ -273,6 +286,7 @@ class ReicheltProvider implements InfoProviderInterface ProviderCapabilities::PICTURE, ProviderCapabilities::DATASHEET, ProviderCapabilities::PRICE, + ProviderCapabilities::GTIN, ]; } } diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php index d5ddc1de..1a3c29c2 100644 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php @@ -77,6 +77,10 @@ final class BarcodeRedirector return $this->getURLVendorBarcode($barcodeScan); } + if ($barcodeScan instanceof GTINBarcodeScanResult) { + return $this->getURLGTINBarcode($barcodeScan); + } + throw new InvalidArgumentException('Unknown $barcodeScan type: '.get_class($barcodeScan)); } @@ -111,6 +115,16 @@ final class BarcodeRedirector return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]); } + private function getURLGTINBarcode(GTINBarcodeScanResult $barcodeScan): string + { + $part = $this->em->getRepository(Part::class)->findOneBy(['gtin' => $barcodeScan->gtin]); + if (!$part instanceof Part) { + throw new EntityNotFoundException(); + } + + return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]); + } + /** * Gets a part from a scan of a Vendor Barcode by filtering for parts * with the same Info Provider Id or, if that fails, by looking for parts with a diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php index e5930b36..520c9f3b 100644 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php @@ -92,6 +92,9 @@ final class BarcodeScanHelper if ($type === BarcodeSourceType::EIGP114) { return $this->parseEIGP114Barcode($input); } + if ($type === BarcodeSourceType::GTIN) { + return $this->parseGTINBarcode($input); + } //Null means auto and we try the different formats $result = $this->parseInternalBarcode($input); @@ -117,9 +120,19 @@ final class BarcodeScanHelper return $result; } + //If the result is a valid GTIN barcode, we can parse it directly + if (GTINBarcodeScanResult::isValidGTIN($input)) { + return $this->parseGTINBarcode($input); + } + throw new InvalidArgumentException('Unknown barcode'); } + private function parseGTINBarcode(string $input): GTINBarcodeScanResult + { + return new GTINBarcodeScanResult($input); + } + private function parseEIGP114Barcode(string $input): EIGP114BarcodeScanResult { return EIGP114BarcodeScanResult::parseFormat06Code($input); diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php index 40f707de..43643d12 100644 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php @@ -42,4 +42,9 @@ enum BarcodeSourceType * EIGP114 formatted barcodes like used by digikey, mouser, etc. */ case EIGP114; -} \ No newline at end of file + + /** + * GTIN /EAN barcodes, which are used on most products in the world. These are checked with the GTIN field of a part. + */ + case GTIN; +} diff --git a/src/Services/LabelSystem/BarcodeScanner/GTINBarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/GTINBarcodeScanResult.php new file mode 100644 index 00000000..30aaa223 --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/GTINBarcodeScanResult.php @@ -0,0 +1,62 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\BarcodeScanner; + +use GtinValidation\GtinValidator; + +readonly class GTINBarcodeScanResult implements BarcodeScanResultInterface +{ + + private GtinValidator $validator; + + public function __construct( + public string $gtin, + ) { + $this->validator = new GtinValidator($this->gtin); + } + + public function getDecodedForInfoMode(): array + { + $obj = $this->validator->getGtinObject(); + return [ + 'GTIN' => $this->gtin, + 'GTIN type' => $obj->getType(), + 'Valid' => $this->validator->isValid() ? 'Yes' : 'No', + ]; + } + + /** + * Checks if the given input is a valid GTIN. This is used to determine whether a scanned barcode should be interpreted as a GTIN or not. + * @param string $input + * @return bool + */ + public static function isValidGTIN(string $input): bool + { + try { + return (new GtinValidator($input))->isValid(); + } catch (\Exception $e) { + return false; + } + } +} diff --git a/src/Services/LabelSystem/LabelHTMLGenerator.php b/src/Services/LabelSystem/LabelHTMLGenerator.php index 8a5201ff..31093953 100644 --- a/src/Services/LabelSystem/LabelHTMLGenerator.php +++ b/src/Services/LabelSystem/LabelHTMLGenerator.php @@ -95,7 +95,7 @@ final class LabelHTMLGenerator 'paper_height' => $options->getHeight(), ] ); - } catch (Error $exception) { + } catch (\Throwable $exception) { throw new TwigModeException($exception); } } else { diff --git a/src/Services/LabelSystem/SandboxedTwigFactory.php b/src/Services/LabelSystem/SandboxedTwigFactory.php index d5e09fa5..fb3b6362 100644 --- a/src/Services/LabelSystem/SandboxedTwigFactory.php +++ b/src/Services/LabelSystem/SandboxedTwigFactory.php @@ -70,12 +70,14 @@ use App\Twig\Sandbox\SandboxedLabelExtension; use App\Twig\TwigCoreExtension; use InvalidArgumentException; use Twig\Environment; +use Twig\Extension\AttributeExtension; use Twig\Extension\SandboxExtension; use Twig\Extra\Html\HtmlExtension; use Twig\Extra\Intl\IntlExtension; use Twig\Extra\Markdown\MarkdownExtension; use Twig\Extra\String\StringExtension; use Twig\Loader\ArrayLoader; +use Twig\RuntimeLoader\FactoryRuntimeLoader; use Twig\Sandbox\SecurityPolicyInterface; /** @@ -84,11 +86,11 @@ use Twig\Sandbox\SecurityPolicyInterface; */ final class SandboxedTwigFactory { - private const ALLOWED_TAGS = ['apply', 'autoescape', 'do', 'for', 'if', 'set', 'verbatim', 'with']; + private const ALLOWED_TAGS = ['apply', 'autoescape', 'do', 'for', 'if', 'set', 'types', 'verbatim', 'with']; private const ALLOWED_FILTERS = ['abs', 'batch', 'capitalize', 'column', 'country_name', - 'currency_name', 'currency_symbol', 'date', 'date_modify', 'data_uri', 'default', 'escape', 'filter', 'first', 'format', + 'currency_name', 'currency_symbol', 'date', 'date_modify', 'data_uri', 'default', 'escape', 'filter', 'find', 'first', 'format', 'format_currency', 'format_date', 'format_datetime', 'format_number', 'format_time', 'html_to_markdown', 'join', 'keys', - 'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'markdown_to_html', 'merge', 'nl2br', 'raw', 'number_format', + 'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'markdown_to_html', 'merge', 'nl2br', 'number_format', 'raw', 'reduce', 'replace', 'reverse', 'round', 'slice', 'slug', 'sort', 'spaceless', 'split', 'striptags', 'timezone_name', 'title', 'trim', 'u', 'upper', 'url_encode', @@ -102,16 +104,17 @@ final class SandboxedTwigFactory ]; private const ALLOWED_FUNCTIONS = ['country_names', 'country_timezones', 'currency_names', 'cycle', - 'date', 'html_classes', 'language_names', 'locale_names', 'max', 'min', 'random', 'range', 'script_names', - 'template_from_string', 'timezone_names', + 'date', 'enum', 'enum_cases', 'html_classes', 'language_names', 'locale_names', 'max', 'min', 'random', 'range', 'script_names', + 'timezone_names', //Part-DB specific extensions: //EntityExtension: - 'entity_type', 'entity_url', + 'entity_type', 'entity_url', 'type_label', 'type_label_plural', //BarcodeExtension: 'barcode_svg', //SandboxedLabelExtension 'placeholder', + 'associated_parts', 'associated_parts_count', 'associated_parts_r', 'associated_parts_count_r', ]; private const ALLOWED_METHODS = [ @@ -128,7 +131,7 @@ final class SandboxedTwigFactory 'getValueTypical', 'getUnit', 'getValueText', ], MeasurementUnit::class => ['getUnit', 'isInteger', 'useSIPrefix'], PartLot::class => ['isExpired', 'getDescription', 'getComment', 'getExpirationDate', 'getStorageLocation', - 'getPart', 'isInstockUnknown', 'getAmount', 'getNeedsRefill', 'getVendorBarcode'], + 'getPart', 'isInstockUnknown', 'getAmount', 'getOwner', 'getLastStocktakeAt', 'getNeedsRefill', 'getVendorBarcode'], StorageLocation::class => ['isFull', 'isOnlySinglePart', 'isLimitToExistingParts', 'getStorageType'], Supplier::class => ['getShippingCosts', 'getDefaultCurrency'], Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getIpn', 'getProviderReference', @@ -139,13 +142,13 @@ final class SandboxedTwigFactory 'getParameters', 'getGroupedParameters', 'isProjectBuildPart', 'getBuiltProject', 'getAssociatedPartsAsOwner', 'getAssociatedPartsAsOther', 'getAssociatedPartsAll', - 'getEdaInfo' + 'getEdaInfo', 'getGtin' ], Currency::class => ['getIsoCode', 'getInverseExchangeRate', 'getExchangeRate'], Orderdetail::class => ['getPart', 'getSupplier', 'getSupplierPartNr', 'getObsolete', - 'getPricedetails', 'findPriceForQty', 'isObsolete', 'getSupplierProductUrl'], + 'getPricedetails', 'findPriceForQty', 'isObsolete', 'getSupplierProductUrl', 'getPricesIncludesVAT'], Pricedetail::class => ['getOrderdetail', 'getPrice', 'getPricePerUnit', 'getPriceRelatedQuantity', - 'getMinDiscountQuantity', 'getCurrency', 'getCurrencyISOCode'], + 'getMinDiscountQuantity', 'getCurrency', 'getCurrencyISOCode', 'getIncludesVat'], InfoProviderReference:: class => ['getProviderKey', 'getProviderId', 'getProviderUrl', 'getLastUpdated', 'isProviderCreated'], PartAssociation::class => ['getType', 'getComment', 'getOwner', 'getOther', 'getOtherType'], @@ -186,13 +189,18 @@ final class SandboxedTwigFactory $twig->addExtension(new StringExtension()); $twig->addExtension(new HtmlExtension()); - //Add Part-DB specific extension - $twig->addExtension($this->formatExtension); - $twig->addExtension($this->barcodeExtension); - $twig->addExtension($this->entityExtension); - $twig->addExtension($this->twigCoreExtension); $twig->addExtension($this->sandboxedLabelExtension); + //Our other extensions are using attributes, we need a bit more work to load them + $dynamicExtensions = [$this->formatExtension, $this->barcodeExtension, $this->entityExtension, $this->twigCoreExtension]; + $dynamicExtensionsMap = []; + + foreach ($dynamicExtensions as $extension) { + $twig->addExtension(new AttributeExtension($extension::class)); + $dynamicExtensionsMap[$extension::class] = static fn () => $extension; + } + $twig->addRuntimeLoader(new FactoryRuntimeLoader($dynamicExtensionsMap)); + return $twig; } diff --git a/src/Services/LogSystem/LogDiffFormatter.php b/src/Services/LogSystem/LogDiffFormatter.php index 8b165d5a..1ac5a2f5 100644 --- a/src/Services/LogSystem/LogDiffFormatter.php +++ b/src/Services/LogSystem/LogDiffFormatter.php @@ -32,7 +32,7 @@ class LogDiffFormatter * @param $old_data * @param $new_data */ - public function formatDiff($old_data, $new_data): string + public function formatDiff(mixed $old_data, mixed $new_data): string { if (is_string($old_data) && is_string($new_data)) { return $this->diffString($old_data, $new_data); diff --git a/src/Services/Parts/PartLotWithdrawAddHelper.php b/src/Services/Parts/PartLotWithdrawAddHelper.php index 34ec4c1d..d6a95b34 100644 --- a/src/Services/Parts/PartLotWithdrawAddHelper.php +++ b/src/Services/Parts/PartLotWithdrawAddHelper.php @@ -197,4 +197,45 @@ final class PartLotWithdrawAddHelper $this->entityManager->remove($origin); } } + + /** + * Perform a stocktake for the given part lot, setting the amount to the given actual amount. + * Please note that the changes are not flushed to DB yet, you have to do this yourself + * @param PartLot $lot + * @param float $actualAmount + * @param string|null $comment + * @param \DateTimeInterface|null $action_timestamp + * @return void + */ + public function stocktake(PartLot $lot, float $actualAmount, ?string $comment = null, ?\DateTimeInterface $action_timestamp = null): void + { + if ($actualAmount < 0) { + throw new \InvalidArgumentException('Actual amount must be non-negative'); + } + + $part = $lot->getPart(); + + //Check whether we have to round the amount + if (!$part->useFloatAmount()) { + $actualAmount = round($actualAmount); + } + + $oldAmount = $lot->getAmount(); + //Clear any unknown status when doing a stocktake, as we now have a known amount + $lot->setInstockUnknown(false); + $lot->setAmount($actualAmount); + if ($action_timestamp) { + $lot->setLastStocktakeAt(\DateTimeImmutable::createFromInterface($action_timestamp)); + } else { + $lot->setLastStocktakeAt(new \DateTimeImmutable()); //Use now if no timestamp is given + } + + $event = PartStockChangedLogEntry::stocktake($lot, $oldAmount, $lot->getAmount(), $part->getAmountSum() , $comment, $action_timestamp); + $this->eventLogger->log($event); + + //Apply the comment also to global events, so it gets associated with the elementChanged log entry + if (!$this->eventCommentHelper->isMessageSet() && ($comment !== null && $comment !== '')) { + $this->eventCommentHelper->setMessage($comment); + } + } } diff --git a/src/Services/System/BackupManager.php b/src/Services/System/BackupManager.php index 9bdc7f71..4946bc24 100644 --- a/src/Services/System/BackupManager.php +++ b/src/Services/System/BackupManager.php @@ -22,6 +22,8 @@ declare(strict_types=1); namespace App\Services\System; +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Shivas\VersioningBundle\Service\VersionManagerInterface; @@ -334,7 +336,7 @@ readonly class BackupManager $params = $connection->getParams(); $platform = $connection->getDatabasePlatform(); - if ($platform instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform) { + if ($platform instanceof AbstractMySQLPlatform) { // Use mysql command to import - need to use shell to handle input redirection $mysqlCmd = 'mysql'; if (isset($params['host'])) { @@ -361,7 +363,7 @@ readonly class BackupManager if (!$process->isSuccessful()) { throw new \RuntimeException('MySQL import failed: ' . $process->getErrorOutput()); } - } elseif ($platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform) { + } elseif ($platform instanceof PostgreSQLPlatform) { // Use psql command to import $psqlCmd = 'psql'; if (isset($params['host'])) { diff --git a/src/Services/UserSystem/PermissionSchemaUpdater.php b/src/Services/UserSystem/PermissionSchemaUpdater.php index 104800dc..fd85ee7c 100644 --- a/src/Services/UserSystem/PermissionSchemaUpdater.php +++ b/src/Services/UserSystem/PermissionSchemaUpdater.php @@ -157,4 +157,20 @@ class PermissionSchemaUpdater $permissions->setPermissionValue('system', 'show_updates', $new_value); } } + + private function upgradeSchemaToVersion4(HasPermissionsInterface $holder): void //@phpstan-ignore-line This is called via reflection + { + $permissions = $holder->getPermissions(); + + //If the reports.generate permission is not defined yet, set it to the value of reports.read + if (!$permissions->isPermissionSet('parts_stock', 'stocktake')) { + //Set the new permission to true only if both add and withdraw are allowed + $new_value = TrinaryLogicHelper::and( + $permissions->getPermissionValue('parts_stock', 'withdraw'), + $permissions->getPermissionValue('parts_stock', 'add') + ); + + $permissions->setPermissionValue('parts_stock', 'stocktake', $new_value); + } + } } diff --git a/src/Services/UserSystem/UserAvatarHelper.php b/src/Services/UserSystem/UserAvatarHelper.php index 9dbe9c12..a1a69cb9 100644 --- a/src/Services/UserSystem/UserAvatarHelper.php +++ b/src/Services/UserSystem/UserAvatarHelper.php @@ -154,6 +154,7 @@ class UserAvatarHelper $attachment_type = new AttachmentType(); $attachment_type->setName('Avatars'); $attachment_type->setFiletypeFilter('image/*'); + $attachment_type->setAllowedTargets([UserAttachment::class]); $this->entityManager->persist($attachment_type); } diff --git a/src/Settings/BehaviorSettings/PartTableColumns.php b/src/Settings/BehaviorSettings/PartTableColumns.php index c025c952..2ea66525 100644 --- a/src/Settings/BehaviorSettings/PartTableColumns.php +++ b/src/Settings/BehaviorSettings/PartTableColumns.php @@ -48,6 +48,7 @@ enum PartTableColumns : string implements TranslatableInterface case MPN = "manufacturer_product_number"; case CUSTOM_PART_STATE = 'partCustomState'; case MASS = "mass"; + case GTIN = "gtin"; case TAGS = "tags"; case ATTACHMENTS = "attachments"; case EDIT = "edit"; diff --git a/src/Settings/SystemSettings/LocalizationSettings.php b/src/Settings/SystemSettings/LocalizationSettings.php index c6780c6c..d0c3ce75 100644 --- a/src/Settings/SystemSettings/LocalizationSettings.php +++ b/src/Settings/SystemSettings/LocalizationSettings.php @@ -25,6 +25,7 @@ namespace App\Settings\SystemSettings; use App\Form\Settings\LanguageMenuEntriesType; use App\Form\Type\LocaleSelectType; +use App\Form\Type\TriStateCheckboxType; use App\Settings\SettingsIcon; use Jbtronics\SettingsBundle\Metadata\EnvVarMode; use Jbtronics\SettingsBundle\ParameterTypes\ArrayType; @@ -46,7 +47,7 @@ class LocalizationSettings #[Assert\Locale()] #[Assert\NotBlank()] #[SettingsParameter(label: new TM("settings.system.localization.locale"), formType: LocaleSelectType::class, - envVar: "string:DEFAULT_LANG", envVarMode: EnvVarMode::OVERWRITE)] + envVar: "string:DEFAULT_LANG", envVarMode: EnvVarMode::OVERWRITE)] public string $locale = 'en'; #[Assert\Timezone()] @@ -73,4 +74,14 @@ class LocalizationSettings )] #[Assert\All([new Assert\Locale()])] public array $languageMenuEntries = []; + + #[SettingsParameter(label: new TM("settings.system.localization.prices_include_tax_by_default"), + description: new TM("settings.system.localization.prices_include_tax_by_default.description"), + formType: TriStateCheckboxType::class + )] + /** + * Indicates whether prices should include tax by default. This is used when creating new pricedetails. + * Null means that the VAT state should be indetermine by default. + */ + public ?bool $pricesIncludeTaxByDefault = null; } diff --git a/src/State/LabelGenerationProcessor.php b/src/State/LabelGenerationProcessor.php new file mode 100644 index 00000000..0472bbbd --- /dev/null +++ b/src/State/LabelGenerationProcessor.php @@ -0,0 +1,141 @@ +. + */ + +namespace App\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\ApiResource\LabelGenerationRequest; +use App\Entity\Base\AbstractDBElement; +use App\Entity\LabelSystem\LabelProfile; +use App\Repository\DBElementRepository; +use App\Repository\LabelProfileRepository; +use App\Services\ElementTypeNameGenerator; +use App\Services\LabelSystem\LabelGenerator; +use App\Services\Misc\RangeParser; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +class LabelGenerationProcessor implements ProcessorInterface +{ + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly LabelGenerator $labelGenerator, + private readonly RangeParser $rangeParser, + private readonly ElementTypeNameGenerator $elementTypeNameGenerator, + private readonly Security $security, + ) { + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Response + { + // Check if user has permission to create labels + if (!$this->security->isGranted('@labels.create_labels')) { + throw new AccessDeniedHttpException('You do not have permission to generate labels.'); + } + + if (!$data instanceof LabelGenerationRequest) { + throw new BadRequestHttpException('Invalid request data for label generation.'); + } + + /** @var LabelGenerationRequest $request */ + $request = $data; + + // Fetch the label profile + /** @var LabelProfileRepository $profileRepo */ + $profileRepo = $this->entityManager->getRepository(LabelProfile::class); + $profile = $profileRepo->find($request->profileId); + if (!$profile instanceof LabelProfile) { + throw new NotFoundHttpException(sprintf('Label profile with ID %d not found.', $request->profileId)); + } + + // Check if user has read permission for the profile + if (!$this->security->isGranted('read', $profile)) { + throw new AccessDeniedHttpException('You do not have permission to access this label profile.'); + } + + // Get label options from profile + $options = $profile->getOptions(); + + // Override element type if provided, otherwise use profile's default + if ($request->elementType !== null) { + $options->setSupportedElement($request->elementType); + } + + // Parse element IDs from the range string + try { + $idArray = $this->rangeParser->parse($request->elementIds); + } catch (\InvalidArgumentException $e) { + throw new BadRequestHttpException('Invalid element IDs format: ' . $e->getMessage()); + } + + if (empty($idArray)) { + throw new BadRequestHttpException('No valid element IDs provided.'); + } + + // Fetch the target entities + /** @var DBElementRepository $repo */ + $repo = $this->entityManager->getRepository($options->getSupportedElement()->getEntityClass()); + + $elements = $repo->getElementsFromIDArray($idArray); + + if (empty($elements)) { + throw new NotFoundHttpException('No elements found with the provided IDs.'); + } + + // Generate the PDF + try { + $pdfContent = $this->labelGenerator->generateLabel($options, $elements); + } catch (\Exception $e) { + throw new BadRequestHttpException('Failed to generate label: ' . $e->getMessage()); + } + + // Generate filename + $filename = $this->generateFilename($elements[0], $profile); + + + // Return PDF as response + return new Response( + $pdfContent, + Response::HTTP_OK, + [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => sprintf('attachment; filename="%s"', $filename), + 'Content-Length' => (string) strlen($pdfContent), + ] + ); + } + + private function generateFilename(AbstractDBElement $element, LabelProfile $profile): string + { + $ret = 'label_' . $this->elementTypeNameGenerator->typeLabel($element); + $ret .= $element->getID(); + $ret .= '_' . preg_replace('/[^a-z0-9_\-]/i', '_', $profile->getName()); + + return $ret . '.pdf'; + } +} diff --git a/src/Twig/AttachmentExtension.php b/src/Twig/AttachmentExtension.php index 9f81abe6..3d5ec611 100644 --- a/src/Twig/AttachmentExtension.php +++ b/src/Twig/AttachmentExtension.php @@ -25,22 +25,34 @@ namespace App\Twig; use App\Entity\Attachments\Attachment; use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Misc\FAIconGenerator; +use Twig\Attribute\AsTwigFunction; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; -final class AttachmentExtension extends AbstractExtension +final readonly class AttachmentExtension { - public function __construct(protected AttachmentURLGenerator $attachmentURLGenerator, protected FAIconGenerator $FAIconGenerator) + public function __construct(private AttachmentURLGenerator $attachmentURLGenerator, private FAIconGenerator $FAIconGenerator) { } - public function getFunctions(): array + /** + * Returns the URL of the thumbnail of the given attachment. Returns null if no thumbnail is available. + */ + #[AsTwigFunction("attachment_thumbnail")] + public function attachmentThumbnail(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string { - return [ - /* Returns the URL to a thumbnail of the given attachment */ - new TwigFunction('attachment_thumbnail', fn(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string => $this->attachmentURLGenerator->getThumbnailURL($attachment, $filter_name)), - /* Returns the font awesome icon class which is representing the given file extension (We allow null here for attachments without extension) */ - new TwigFunction('ext_to_fa_icon', fn(?string $extension): string => $this->FAIconGenerator->fileExtensionToFAType($extension ?? '')), - ]; + return $this->attachmentURLGenerator->getThumbnailURL($attachment, $filter_name); + } + + /** + * Return the font-awesome icon type for the given file extension. Returns "file" if no specific icon is available. + * Null is allowed for files withot extension + * @param string|null $extension + * @return string + */ + #[AsTwigFunction("ext_to_fa_icon")] + public function extToFAIcon(?string $extension): string + { + return $this->FAIconGenerator->fileExtensionToFAType($extension ?? ''); } } diff --git a/src/Twig/BarcodeExtension.php b/src/Twig/BarcodeExtension.php index ae1973e3..25c0d78e 100644 --- a/src/Twig/BarcodeExtension.php +++ b/src/Twig/BarcodeExtension.php @@ -23,19 +23,14 @@ declare(strict_types=1); namespace App\Twig; use Com\Tecnick\Barcode\Barcode; -use Twig\Extension\AbstractExtension; -use Twig\TwigFunction; +use Twig\Attribute\AsTwigFunction; -final class BarcodeExtension extends AbstractExtension +final class BarcodeExtension { - public function getFunctions(): array - { - return [ - /* Generates a barcode with the given Type and Data and returns it as an SVG represenation */ - new TwigFunction('barcode_svg', fn(string $content, string $type = 'QRCODE'): string => $this->barcodeSVG($content, $type)), - ]; - } - + /** + * Generates a barcode in SVG format for the given content and type. + */ + #[AsTwigFunction('barcode_svg')] public function barcodeSVG(string $content, string $type = 'QRCODE'): string { $barcodeFactory = new Barcode(); diff --git a/src/Twig/EntityExtension.php b/src/Twig/EntityExtension.php index 427a39b5..bff21eb8 100644 --- a/src/Twig/EntityExtension.php +++ b/src/Twig/EntityExtension.php @@ -41,6 +41,9 @@ use App\Exceptions\EntityNotSupportedException; use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; use App\Services\Trees\TreeViewGenerator; +use Twig\Attribute\AsTwigFunction; +use Twig\Attribute\AsTwigTest; +use Twig\DeprecatedCallableInfo; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; use Twig\TwigTest; @@ -48,61 +51,27 @@ use Twig\TwigTest; /** * @see \App\Tests\Twig\EntityExtensionTest */ -final class EntityExtension extends AbstractExtension +final readonly class EntityExtension { - public function __construct(protected EntityURLGenerator $entityURLGenerator, protected TreeViewGenerator $treeBuilder, private readonly ElementTypeNameGenerator $nameGenerator) + public function __construct(private EntityURLGenerator $entityURLGenerator, private TreeViewGenerator $treeBuilder, private ElementTypeNameGenerator $nameGenerator) { } - public function getTests(): array + /** + * Checks if the given variable is an entity (instance of AbstractDBElement). + */ + #[AsTwigTest("entity")] + public function entityTest(mixed $var): bool { - return [ - /* Checks if the given variable is an entitity (instance of AbstractDBElement) */ - new TwigTest('entity', static fn($var) => $var instanceof AbstractDBElement), - ]; + return $var instanceof AbstractDBElement; } - public function getFunctions(): array - { - return [ - /* Returns a string representation of the given entity */ - new TwigFunction('entity_type', fn(object $entity): ?string => $this->getEntityType($entity)), - /* Returns the URL to the given entity */ - new TwigFunction('entity_url', fn(AbstractDBElement $entity, string $method = 'info'): string => $this->generateEntityURL($entity, $method)), - /* Returns the URL to the given entity in timetravel mode */ - new TwigFunction('timetravel_url', fn(AbstractDBElement $element, \DateTimeInterface $dateTime): ?string => $this->timeTravelURL($element, $dateTime)), - /* Generates a JSON array of the given tree */ - new TwigFunction('tree_data', fn(AbstractDBElement $element, string $type = 'newEdit'): string => $this->treeData($element, $type)), - /* Gets a human readable label for the type of the given entity */ - new TwigFunction('entity_type_label', fn(object|string $entity): string => $this->nameGenerator->getLocalizedTypeLabel($entity)), - new TwigFunction('type_label', fn(object|string $entity): string => $this->nameGenerator->typeLabel($entity)), - new TwigFunction('type_label_p', fn(object|string $entity): string => $this->nameGenerator->typeLabelPlural($entity)), - ]; - } - - public function timeTravelURL(AbstractDBElement $element, \DateTimeInterface $dateTime): ?string - { - try { - return $this->entityURLGenerator->timeTravelURL($element, $dateTime); - } catch (EntityNotSupportedException) { - return null; - } - } - - public function treeData(AbstractDBElement $element, string $type = 'newEdit'): string - { - $tree = $this->treeBuilder->getTreeView($element::class, null, $type, $element); - - return json_encode($tree, JSON_THROW_ON_ERROR); - } - - public function generateEntityURL(AbstractDBElement $entity, string $method = 'info'): string - { - return $this->entityURLGenerator->getURL($entity, $method); - } - - public function getEntityType(object $entity): ?string + /** + * Returns a string representation of the given entity + */ + #[AsTwigFunction("entity_type")] + public function entityType(object $entity): ?string { $map = [ Part::class => 'part', @@ -129,4 +98,69 @@ final class EntityExtension extends AbstractExtension return null; } + + /** + * Returns the URL for the given entity and method. E.g. for a Part and method "edit", it will return the URL to edit this part. + */ + #[AsTwigFunction("entity_url")] + public function entityURL(AbstractDBElement $entity, string $method = 'info'): string + { + return $this->entityURLGenerator->getURL($entity, $method); + } + + + /** + * Returns the URL for the given entity in timetravel mode. + */ + #[AsTwigFunction("timetravel_url")] + public function timeTravelURL(AbstractDBElement $element, \DateTimeInterface $dateTime): ?string + { + try { + return $this->entityURLGenerator->timeTravelURL($element, $dateTime); + } catch (EntityNotSupportedException) { + return null; + } + } + + /** + * Generates a tree data structure for the given element, which can be used to display a tree view of the element and its related entities. + * The type parameter can be used to specify the type of tree view (e.g. "newEdit" for the tree view in the new/edit pages). The returned data is a JSON string. + */ + #[AsTwigFunction("tree_data")] + public function treeData(AbstractDBElement $element, string $type = 'newEdit'): string + { + $tree = $this->treeBuilder->getTreeView($element::class, null, $type, $element); + + return json_encode($tree, JSON_THROW_ON_ERROR); + } + + /** + * Gets the localized type label for the given entity. E.g. for a Part, it will return "Part" in English and "Bauteil" in German. + * @deprecated Use the "type_label" function instead, which does the same but is more concise. + */ + #[AsTwigFunction("entity_type_label", deprecationInfo: new DeprecatedCallableInfo("Part-DB", "2", "Use the 'type_label' function instead."))] + public function entityTypeLabel(object|string $entity): string + { + return $this->nameGenerator->getLocalizedTypeLabel($entity); + } + + /** + * Gets the localized type label for the given entity. E.g. for a Part, it will return "Part" in English and "Bauteil" in German. + */ + #[AsTwigFunction("type_label")] + public function typeLabel(object|string $entity): string + { + return $this->nameGenerator->typeLabel($entity); + } + + /** + * Gets the localized plural type label for the given entity. E.g. for a Part, it will return "Parts" in English and "Bauteile" in German. + * @param object|string $entity + * @return string + */ + #[AsTwigFunction("type_label_p")] + public function typeLabelPlural(object|string $entity): string + { + return $this->nameGenerator->typeLabelPlural($entity); + } } diff --git a/src/Twig/FormatExtension.php b/src/Twig/FormatExtension.php index 46313aaf..b91b7e11 100644 --- a/src/Twig/FormatExtension.php +++ b/src/Twig/FormatExtension.php @@ -29,35 +29,28 @@ use App\Services\Formatters\MarkdownParser; use App\Services\Formatters\MoneyFormatter; use App\Services\Formatters\SIFormatter; use Brick\Math\BigDecimal; -use Twig\Extension\AbstractExtension; -use Twig\TwigFilter; +use Twig\Attribute\AsTwigFilter; -final class FormatExtension extends AbstractExtension +final readonly class FormatExtension { - public function __construct(protected MarkdownParser $markdownParser, protected MoneyFormatter $moneyFormatter, protected SIFormatter $siformatter, protected AmountFormatter $amountFormatter) + public function __construct(private MarkdownParser $markdownParser, private MoneyFormatter $moneyFormatter, private SIFormatter $siformatter, private AmountFormatter $amountFormatter) { } - public function getFilters(): array + /** + * Mark the given text as markdown, which will be rendered in the browser + */ + #[AsTwigFilter("format_markdown", isSafe: ['html'], preEscape: 'html')] + public function formatMarkdown(string $markdown, bool $inline_mode = false): string { - return [ - /* Mark the given text as markdown, which will be rendered in the browser */ - new TwigFilter('format_markdown', fn(string $markdown, bool $inline_mode = false): string => $this->markdownParser->markForRendering($markdown, $inline_mode), [ - 'pre_escape' => 'html', - 'is_safe' => ['html'], - ]), - /* Format the given amount as money, using a given currency */ - new TwigFilter('format_money', fn($amount, ?Currency $currency = null, int $decimals = 5): string => $this->formatCurrency($amount, $currency, $decimals)), - /* Format the given number using SI prefixes and the given unit (string) */ - new TwigFilter('format_si', fn($value, $unit, $decimals = 2, bool $show_all_digits = false): string => $this->siFormat($value, $unit, $decimals, $show_all_digits)), - /** Format the given amount using the given MeasurementUnit */ - new TwigFilter('format_amount', fn($value, ?MeasurementUnit $unit, array $options = []): string => $this->amountFormat($value, $unit, $options)), - /** Format the given number of bytes as human-readable number */ - new TwigFilter('format_bytes', fn(int $bytes, int $precision = 2): string => $this->formatBytes($bytes, $precision)), - ]; + return $this->markdownParser->markForRendering($markdown, $inline_mode); } - public function formatCurrency($amount, ?Currency $currency = null, int $decimals = 5): string + /** + * Format the given amount as money, using a given currency + */ + #[AsTwigFilter("format_money")] + public function formatMoney(BigDecimal|float|string $amount, ?Currency $currency = null, int $decimals = 5): string { if ($amount instanceof BigDecimal) { $amount = (string) $amount; @@ -66,19 +59,22 @@ final class FormatExtension extends AbstractExtension return $this->moneyFormatter->format($amount, $currency, $decimals); } - public function siFormat($value, $unit, $decimals = 2, bool $show_all_digits = false): string + /** + * Format the given number using SI prefixes and the given unit (string) + */ + #[AsTwigFilter("format_si")] + public function siFormat(float $value, string $unit, int $decimals = 2, bool $show_all_digits = false): string { return $this->siformatter->format($value, $unit, $decimals); } - public function amountFormat($value, ?MeasurementUnit $unit, array $options = []): string + #[AsTwigFilter("format_amount")] + public function amountFormat(float|int|string $value, ?MeasurementUnit $unit, array $options = []): string { return $this->amountFormatter->format($value, $unit, $options); } - /** - * @param $bytes - */ + #[AsTwigFilter("format_bytes")] public function formatBytes(int $bytes, int $precision = 2): string { $size = ['B','kB','MB','GB','TB','PB','EB','ZB','YB']; diff --git a/src/Twig/InfoProviderExtension.php b/src/Twig/InfoProviderExtension.php index a963b778..54dbf93a 100644 --- a/src/Twig/InfoProviderExtension.php +++ b/src/Twig/InfoProviderExtension.php @@ -23,31 +23,25 @@ declare(strict_types=1); namespace App\Twig; +use Twig\Attribute\AsTwigFunction; use App\Services\InfoProviderSystem\ProviderRegistry; use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; -class InfoProviderExtension extends AbstractExtension +final readonly class InfoProviderExtension { public function __construct( - private readonly ProviderRegistry $providerRegistry + private ProviderRegistry $providerRegistry ) {} - public function getFunctions(): array - { - return [ - new TwigFunction('info_provider', $this->getInfoProvider(...)), - new TwigFunction('info_provider_label', $this->getInfoProviderName(...)) - ]; - } - /** * Gets the info provider with the given key. Returns null, if the provider does not exist. * @param string $key * @return InfoProviderInterface|null */ - private function getInfoProvider(string $key): ?InfoProviderInterface + #[AsTwigFunction(name: 'info_provider')] + public function getInfoProvider(string $key): ?InfoProviderInterface { try { return $this->providerRegistry->getProviderByKey($key); @@ -61,7 +55,8 @@ class InfoProviderExtension extends AbstractExtension * @param string $key * @return string|null */ - private function getInfoProviderName(string $key): ?string + #[AsTwigFunction(name: 'info_provider_label')] + public function getInfoProviderName(string $key): ?string { try { return $this->providerRegistry->getProviderByKey($key)->getProviderInfo()['name']; @@ -69,4 +64,4 @@ class InfoProviderExtension extends AbstractExtension return null; } } -} \ No newline at end of file +} diff --git a/src/Twig/LogExtension.php b/src/Twig/LogExtension.php index 34dad988..738a24c2 100644 --- a/src/Twig/LogExtension.php +++ b/src/Twig/LogExtension.php @@ -25,21 +25,26 @@ namespace App\Twig; use App\Entity\LogSystem\AbstractLogEntry; use App\Services\LogSystem\LogDataFormatter; use App\Services\LogSystem\LogDiffFormatter; +use Twig\Attribute\AsTwigFunction; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; -final class LogExtension extends AbstractExtension +final readonly class LogExtension { - public function __construct(private readonly LogDataFormatter $logDataFormatter, private readonly LogDiffFormatter $logDiffFormatter) + public function __construct(private LogDataFormatter $logDataFormatter, private LogDiffFormatter $logDiffFormatter) { } - public function getFunctions(): array + #[AsTwigFunction(name: 'format_log_data', isSafe: ['html'])] + public function formatLogData(mixed $data, AbstractLogEntry $logEntry, string $fieldName): string { - return [ - new TwigFunction('format_log_data', fn($data, AbstractLogEntry $logEntry, string $fieldName): string => $this->logDataFormatter->formatData($data, $logEntry, $fieldName), ['is_safe' => ['html']]), - new TwigFunction('format_log_diff', fn($old_data, $new_data): string => $this->logDiffFormatter->formatDiff($old_data, $new_data), ['is_safe' => ['html']]), - ]; + return $this->logDataFormatter->formatData($data, $logEntry, $fieldName); + } + + #[AsTwigFunction(name: 'format_log_diff', isSafe: ['html'])] + public function formatLogDiff(mixed $old_data, mixed $new_data): string + { + return $this->logDiffFormatter->formatDiff($old_data, $new_data); } } diff --git a/src/Twig/MiscExtension.php b/src/Twig/MiscExtension.php index 8b6ebc68..390ad084 100644 --- a/src/Twig/MiscExtension.php +++ b/src/Twig/MiscExtension.php @@ -22,6 +22,7 @@ declare(strict_types=1); */ namespace App\Twig; +use Twig\Attribute\AsTwigFunction; use App\Settings\SettingsIcon; use Symfony\Component\HttpFoundation\Request; use App\Services\LogSystem\EventCommentType; @@ -31,23 +32,14 @@ use Twig\TwigFunction; use App\Services\LogSystem\EventCommentNeededHelper; use Twig\Extension\AbstractExtension; -final class MiscExtension extends AbstractExtension +final readonly class MiscExtension { - public function __construct(private readonly EventCommentNeededHelper $eventCommentNeededHelper) + public function __construct(private EventCommentNeededHelper $eventCommentNeededHelper) { } - public function getFunctions(): array - { - return [ - new TwigFunction('event_comment_needed', $this->evenCommentNeeded(...)), - - new TwigFunction('settings_icon', $this->settingsIcon(...)), - new TwigFunction('uri_without_host', $this->uri_without_host(...)) - ]; - } - - private function evenCommentNeeded(string|EventCommentType $operation_type): bool + #[AsTwigFunction(name: 'event_comment_needed')] + public function evenCommentNeeded(string|EventCommentType $operation_type): bool { if (is_string($operation_type)) { $operation_type = EventCommentType::from($operation_type); @@ -63,7 +55,8 @@ final class MiscExtension extends AbstractExtension * @return string|null * @throws \ReflectionException */ - private function settingsIcon(string|object $objectOrClass): ?string + #[AsTwigFunction(name: 'settings_icon')] + public function settingsIcon(string|object $objectOrClass): ?string { //If the given object is a proxy, then get the real object if (is_a($objectOrClass, SettingsProxyInterface::class)) { @@ -82,6 +75,7 @@ final class MiscExtension extends AbstractExtension * @param Request $request * @return string */ + #[AsTwigFunction(name: 'uri_without_host')] public function uri_without_host(Request $request): string { if (null !== $qs = $request->getQueryString()) { diff --git a/src/Twig/Sandbox/SandboxedLabelExtension.php b/src/Twig/Sandbox/SandboxedLabelExtension.php index 59fb0af0..6fe85e80 100644 --- a/src/Twig/Sandbox/SandboxedLabelExtension.php +++ b/src/Twig/Sandbox/SandboxedLabelExtension.php @@ -23,14 +23,18 @@ declare(strict_types=1); namespace App\Twig\Sandbox; +use App\Entity\Base\AbstractPartsContainingDBElement; +use App\Entity\Parts\Part; +use App\Repository\AbstractPartsContainingRepository; use App\Services\LabelSystem\LabelTextReplacer; +use Doctrine\ORM\EntityManagerInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; class SandboxedLabelExtension extends AbstractExtension { - public function __construct(private readonly LabelTextReplacer $labelTextReplacer) + public function __construct(private readonly LabelTextReplacer $labelTextReplacer, private readonly EntityManagerInterface $em) { } @@ -39,6 +43,11 @@ class SandboxedLabelExtension extends AbstractExtension { return [ new TwigFunction('placeholder', fn(string $text, object $label_target) => $this->labelTextReplacer->handlePlaceholderOrReturnNull($text, $label_target)), + + new TwigFunction("associated_parts", $this->associatedParts(...)), + new TwigFunction("associated_parts_count", $this->associatedPartsCount(...)), + new TwigFunction("associated_parts_r", $this->associatedPartsRecursive(...)), + new TwigFunction("associated_parts_count_r", $this->associatedPartsCountRecursive(...)), ]; } @@ -48,4 +57,37 @@ class SandboxedLabelExtension extends AbstractExtension new TwigFilter('placeholders', fn(string $text, object $label_target) => $this->labelTextReplacer->replace($text, $label_target)), ]; } -} \ No newline at end of file + + /** + * Returns all parts associated with the given element. + * @param AbstractPartsContainingDBElement $element + * @return Part[] + */ + public function associatedParts(AbstractPartsContainingDBElement $element): array + { + /** @var AbstractPartsContainingRepository $repo */ + $repo = $this->em->getRepository($element::class); + return $repo->getParts($element); + } + + public function associatedPartsCount(AbstractPartsContainingDBElement $element): int + { + /** @var AbstractPartsContainingRepository $repo */ + $repo = $this->em->getRepository($element::class); + return $repo->getPartsCount($element); + } + + public function associatedPartsRecursive(AbstractPartsContainingDBElement $element): array + { + /** @var AbstractPartsContainingRepository $repo */ + $repo = $this->em->getRepository($element::class); + return $repo->getPartsRecursive($element); + } + + public function associatedPartsCountRecursive(AbstractPartsContainingDBElement $element): int + { + /** @var AbstractPartsContainingRepository $repo */ + $repo = $this->em->getRepository($element::class); + return $repo->getPartsCountRecursive($element); + } +} diff --git a/src/Twig/TwigCoreExtension.php b/src/Twig/TwigCoreExtension.php index 7b2b58f8..ceb4ce82 100644 --- a/src/Twig/TwigCoreExtension.php +++ b/src/Twig/TwigCoreExtension.php @@ -22,7 +22,11 @@ declare(strict_types=1); */ namespace App\Twig; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Twig\Attribute\AsTwigFilter; +use Twig\Attribute\AsTwigFunction; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Twig\Attribute\AsTwigTest; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -32,58 +36,54 @@ use Twig\TwigTest; * The functionalities here extend the Twig with some core functions, which are independently of Part-DB. * @see \App\Tests\Twig\TwigCoreExtensionTest */ -final class TwigCoreExtension extends AbstractExtension +final readonly class TwigCoreExtension { - private readonly ObjectNormalizer $objectNormalizer; + private NormalizerInterface $objectNormalizer; public function __construct() { $this->objectNormalizer = new ObjectNormalizer(); } - public function getFunctions(): array + /** + * Checks if the given variable is an instance of the given class/interface/enum. E.g. `x is instanceof('App\Entity\Parts\Part')` + * @param mixed $var + * @param string $instance + * @return bool + */ + #[AsTwigTest("instanceof")] + public function testInstanceOf(mixed $var, string $instance): bool { - return [ - /* Returns the enum cases as values */ - new TwigFunction('enum_cases', $this->getEnumCases(...)), - ]; - } + if (!class_exists($instance) && !interface_exists($instance) && !enum_exists($instance)) { + throw new \InvalidArgumentException(sprintf('The given class/interface/enum "%s" does not exist!', $instance)); + } - public function getTests(): array - { - return [ - /* - * Checks if a given variable is an instance of a given class. E.g. ` x is instanceof('App\Entity\Parts\Part')` - */ - new TwigTest('instanceof', static fn($var, $instance) => $var instanceof $instance), - /* Checks if a given variable is an object. E.g. `x is object` */ - new TwigTest('object', static fn($var): bool => is_object($var)), - new TwigTest('enum', fn($var) => $var instanceof \UnitEnum), - ]; + return $var instanceof $instance; } /** - * @param string $enum_class - * @phpstan-param class-string $enum_class + * Checks if the given variable is an object. This can be used to check if a variable is an object, without knowing the exact class of the object. E.g. `x is object` + * @param mixed $var + * @return bool */ - public function getEnumCases(string $enum_class): array + #[AsTwigTest("object")] + public function testObject(mixed $var): bool { - if (!enum_exists($enum_class)) { - throw new \InvalidArgumentException(sprintf('The given class "%s" is not an enum!', $enum_class)); - } - - /** @noinspection PhpUndefinedMethodInspection */ - return ($enum_class)::cases(); + return is_object($var); } - public function getFilters(): array + /** + * Checks if the given variable is an enum (instance of UnitEnum). This can be used to check if a variable is an enum, without knowing the exact class of the enum. E.g. `x is enum` + * @param mixed $var + * @return bool + */ + #[AsTwigTest("enum")] + public function testEnum(mixed $var): bool { - return [ - /* Converts the given object to an array representation of the public/accessible properties */ - new TwigFilter('to_array', fn($object) => $this->toArray($object)), - ]; + return $var instanceof \UnitEnum; } + #[AsTwigFilter('to_array')] public function toArray(object|array $object): array { //If it is already an array, we can just return it diff --git a/src/Twig/UpdateExtension.php b/src/Twig/UpdateExtension.php index ee3bb16c..7ec7897b 100644 --- a/src/Twig/UpdateExtension.php +++ b/src/Twig/UpdateExtension.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Twig; +use Twig\Attribute\AsTwigFunction; use App\Services\System\UpdateAvailableFacade; use Symfony\Bundle\SecurityBundle\Security; use Twig\Extension\AbstractExtension; @@ -31,26 +32,18 @@ use Twig\TwigFunction; /** * Twig extension for update-related functions. */ -final class UpdateExtension extends AbstractExtension +final readonly class UpdateExtension { - public function __construct(private readonly UpdateAvailableFacade $updateAvailableManager, - private readonly Security $security) + public function __construct(private UpdateAvailableFacade $updateAvailableManager, + private Security $security) { } - public function getFunctions(): array - { - return [ - new TwigFunction('is_update_available', $this->isUpdateAvailable(...)), - new TwigFunction('get_latest_version', $this->getLatestVersion(...)), - new TwigFunction('get_latest_version_url', $this->getLatestVersionUrl(...)), - ]; - } - /** * Check if an update is available and the user has permission to see it. */ + #[AsTwigFunction(name: 'is_update_available')] public function isUpdateAvailable(): bool { // Only show to users with the show_updates permission @@ -64,6 +57,7 @@ final class UpdateExtension extends AbstractExtension /** * Get the latest available version string. */ + #[AsTwigFunction(name: 'get_latest_version')] public function getLatestVersion(): string { return $this->updateAvailableManager->getLatestVersionString(); @@ -72,6 +66,7 @@ final class UpdateExtension extends AbstractExtension /** * Get the URL to the latest version release page. */ + #[AsTwigFunction(name: 'get_latest_version_url')] public function getLatestVersionUrl(): string { return $this->updateAvailableManager->getLatestVersionUrl(); diff --git a/src/Twig/UserExtension.php b/src/Twig/UserExtension.php index 5045257a..81ff0857 100644 --- a/src/Twig/UserExtension.php +++ b/src/Twig/UserExtension.php @@ -41,51 +41,24 @@ declare(strict_types=1); namespace App\Twig; -use App\Entity\Base\AbstractDBElement; +use Twig\Attribute\AsTwigFilter; +use Twig\Attribute\AsTwigFunction; use App\Entity\UserSystem\User; -use App\Entity\LogSystem\AbstractLogEntry; -use App\Repository\LogEntryRepository; -use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Twig\Extension\AbstractExtension; -use Twig\TwigFilter; -use Twig\TwigFunction; /** * @see \App\Tests\Twig\UserExtensionTest */ -final class UserExtension extends AbstractExtension +final readonly class UserExtension { - private readonly LogEntryRepository $repo; - public function __construct(EntityManagerInterface $em, - private readonly Security $security, - private readonly UrlGeneratorInterface $urlGenerator) + public function __construct( + private Security $security, + private UrlGeneratorInterface $urlGenerator) { - $this->repo = $em->getRepository(AbstractLogEntry::class); - } - - public function getFilters(): array - { - return [ - new TwigFilter('remove_locale_from_path', fn(string $path): string => $this->removeLocaleFromPath($path)), - ]; - } - - public function getFunctions(): array - { - return [ - /* Returns the user which has edited the given entity the last time. */ - new TwigFunction('last_editing_user', fn(AbstractDBElement $element): ?User => $this->repo->getLastEditingUser($element)), - /* Returns the user which has created the given entity. */ - new TwigFunction('creating_user', fn(AbstractDBElement $element): ?User => $this->repo->getCreatingUser($element)), - new TwigFunction('impersonator_user', $this->getImpersonatorUser(...)), - new TwigFunction('impersonation_active', $this->isImpersonationActive(...)), - new TwigFunction('impersonation_path', $this->getImpersonationPath(...)), - ]; } /** @@ -93,6 +66,7 @@ final class UserExtension extends AbstractExtension * If the current user is not impersonated, null is returned. * @return User|null */ + #[AsTwigFunction(name: 'impersonator_user')] public function getImpersonatorUser(): ?User { $token = $this->security->getToken(); @@ -107,11 +81,13 @@ final class UserExtension extends AbstractExtension return null; } + #[AsTwigFunction(name: 'impersonation_active')] public function isImpersonationActive(): bool { return $this->security->isGranted('IS_IMPERSONATOR'); } + #[AsTwigFunction(name: 'impersonation_path')] public function getImpersonationPath(User $user, string $route_name = 'homepage'): string { if (! $this->security->isGranted('CAN_SWITCH_USER', $user)) { @@ -124,6 +100,7 @@ final class UserExtension extends AbstractExtension /** * This function/filter generates a path. */ + #[AsTwigFilter(name: 'remove_locale_from_path')] public function removeLocaleFromPath(string $path): string { //Ensure the path has the correct format diff --git a/src/Twig/UserRepoExtension.php b/src/Twig/UserRepoExtension.php new file mode 100644 index 00000000..1dcc6706 --- /dev/null +++ b/src/Twig/UserRepoExtension.php @@ -0,0 +1,57 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Twig; + +use App\Entity\Base\AbstractDBElement; +use App\Entity\LogSystem\AbstractLogEntry; +use App\Entity\UserSystem\User; +use App\Repository\LogEntryRepository; +use Doctrine\ORM\EntityManagerInterface; +use Twig\Attribute\AsTwigFunction; + +final readonly class UserRepoExtension +{ + + public function __construct(private EntityManagerInterface $entityManager) + { + } + + /** + * Returns the user which has edited the given entity the last time. + */ + #[AsTwigFunction('creating_user')] + public function creatingUser(AbstractDBElement $element): ?User + { + return $this->entityManager->getRepository(AbstractLogEntry::class)->getCreatingUser($element); + } + + /** + * Returns the user which has edited the given entity the last time. + */ + #[AsTwigFunction('last_editing_user')] + public function lastEditingUser(AbstractDBElement $element): ?User + { + return $this->entityManager->getRepository(AbstractLogEntry::class)->getLastEditingUser($element); + } +} diff --git a/src/Validator/Constraints/UniquePartIpnConstraint.php b/src/Validator/Constraints/UniquePartIpnConstraint.php index ca32f9ef..652f2bcd 100644 --- a/src/Validator/Constraints/UniquePartIpnConstraint.php +++ b/src/Validator/Constraints/UniquePartIpnConstraint.php @@ -1,5 +1,7 @@ . + */ + +declare(strict_types=1); + + +namespace App\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; + +/** + * A constraint to ensure that a GTIN is valid. + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class ValidGTIN extends Constraint +{ + +} diff --git a/src/Validator/Constraints/ValidGTINValidator.php b/src/Validator/Constraints/ValidGTINValidator.php new file mode 100644 index 00000000..57eb23e6 --- /dev/null +++ b/src/Validator/Constraints/ValidGTINValidator.php @@ -0,0 +1,54 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Validator\Constraints; + +use GtinValidation\GtinValidator; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; + +class ValidGTINValidator extends ConstraintValidator +{ + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof ValidGTIN) { + throw new UnexpectedTypeException($constraint, ValidGTIN::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!is_string($value)) { + throw new UnexpectedTypeException($value, 'string'); + } + + $gtinValidator = new GtinValidator($value); + if (!$gtinValidator->isValid()) { + $this->context->buildViolation('validator.invalid_gtin') + ->addViolation(); + } + } +} diff --git a/templates/admin/attachment_type_admin.html.twig b/templates/admin/attachment_type_admin.html.twig index 87a053af..9aeba934 100644 --- a/templates/admin/attachment_type_admin.html.twig +++ b/templates/admin/attachment_type_admin.html.twig @@ -6,6 +6,7 @@ {% block additional_controls %} {{ form_row(form.filetype_filter) }} + {{ form_row(form.allowed_targets) }} {{ form_row(form.alternative_names) }} {% endblock %} diff --git a/templates/form/extended_bootstrap_layout.html.twig b/templates/form/extended_bootstrap_layout.html.twig index 75e44a15..1227750c 100644 --- a/templates/form/extended_bootstrap_layout.html.twig +++ b/templates/form/extended_bootstrap_layout.html.twig @@ -100,6 +100,17 @@ {%- endif -%} {%- endblock tristate_widget %} +{% block tristate_row -%} + {#--#} +
{#--#} +
+ {{- form_widget(form) -}} + {{- form_help(form) -}} + {{- form_errors(form) -}} +
{#--#} + +{%- endblock tristate_row %} + {%- block choice_widget_collapsed -%} {# Only add the BS5 form-select class if we dont use bootstrap-selectpicker #} {# {% if attr["data-controller"] is defined and attr["data-controller"] not in ["elements--selectpicker"] %} @@ -144,3 +155,8 @@ {{- parent() -}} {% endif %} {% endblock %} + +{% block boolean_constraint_widget %} + {{ form_widget(form.value) }} + {{ form_errors(form.value) }} +{% endblock %} diff --git a/templates/helper.twig b/templates/helper.twig index 66268a96..9e68d56c 100644 --- a/templates/helper.twig +++ b/templates/helper.twig @@ -192,7 +192,7 @@ {% set preview_attach = part_preview_generator.tablePreviewAttachment(part) %} {% if preview_attach %} Part image + {{ stimulus_controller('elements/hoverpic') }} data-thumbnail="{{ attachment_thumbnail(preview_attach) }}"> {% endif %} {{ part.name }} {% endmacro %} @@ -241,3 +241,11 @@ {{ datetime|format_datetime }} {% endif %} {% endmacro %} + +{% macro vat_text(bool) %} + {% if bool === true %} + ({% trans %}prices.incl_vat{% endtrans %}) + {% elseif bool === false %} + ({% trans %}prices.excl_vat{% endtrans %}) + {% endif %} +{% endmacro %} diff --git a/templates/info_providers/search/part_search.html.twig b/templates/info_providers/search/part_search.html.twig index 3d741c34..a5602618 100644 --- a/templates/info_providers/search/part_search.html.twig +++ b/templates/info_providers/search/part_search.html.twig @@ -94,7 +94,13 @@ {{ dto.footprint }} {% endif %} - {{ helper.m_status_to_badge(dto.manufacturing_status) }} + + {{ helper.m_status_to_badge(dto.manufacturing_status) }} + {% if dto.gtin %} +
+ {{ dto.gtin }} + {% endif %} + {% if dto.provider_url %} diff --git a/templates/parts/edit/_advanced.html.twig b/templates/parts/edit/_advanced.html.twig index b0f1ff86..30479d11 100644 --- a/templates/parts/edit/_advanced.html.twig +++ b/templates/parts/edit/_advanced.html.twig @@ -13,4 +13,5 @@ {{ form_row(form.ipn) }} {{ form_row(form.partUnit) }} -{{ form_row(form.partCustomState) }} \ No newline at end of file +{{ form_row(form.partCustomState) }} +{{ form_row(form.gtin) }} diff --git a/templates/parts/edit/_eda.html.twig b/templates/parts/edit/_eda.html.twig index 4df675c4..1383871e 100644 --- a/templates/parts/edit/_eda.html.twig +++ b/templates/parts/edit/_eda.html.twig @@ -1,11 +1,7 @@ {{ form_row(form.eda_info.reference_prefix) }} {{ form_row(form.eda_info.value) }} -
-
- {{ form_row(form.eda_info.visibility) }} -
-
+{{ form_row(form.eda_info.visibility) }}
@@ -21,4 +17,4 @@
{{ form_row(form.eda_info.kicad_symbol) }} -{{ form_row(form.eda_info.kicad_footprint) }} \ No newline at end of file +{{ form_row(form.eda_info.kicad_footprint) }} diff --git a/templates/parts/edit/edit_form_styles.html.twig b/templates/parts/edit/edit_form_styles.html.twig index c2a89b6a..844c8700 100644 --- a/templates/parts/edit/edit_form_styles.html.twig +++ b/templates/parts/edit/edit_form_styles.html.twig @@ -32,6 +32,7 @@ {{ form_row(form.supplierpartnr, {'attr': {'class': 'form-control-sm'}}) }} {{ form_row(form.supplier_product_url, {'attr': {'class': 'form-control-sm'}}) }} {{ form_widget(form.obsolete) }} + {{ form_widget(form.pricesIncludesVAT) }}
@@ -109,6 +110,7 @@ {{ form_row(form.comment) }} {{ form_row(form.owner) }} {{ form_row(form.user_barcode) }} + {{ form_row(form.last_stocktake_at) }}
@@ -226,4 +228,4 @@ {{ form_errors(form) }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/parts/info/_extended_infos.html.twig b/templates/parts/info/_extended_infos.html.twig index 4ed60a09..9cb4e4e5 100644 --- a/templates/parts/info/_extended_infos.html.twig +++ b/templates/parts/info/_extended_infos.html.twig @@ -42,6 +42,11 @@ {{ part.ipn ?? 'part.ipn.not_defined'|trans }} + + {% trans %}part.gtin{% endtrans %} + {{ part.gtin ?? '' }} + + {# Favorite status #} {% trans %}part.isFavorite{% endtrans %} {{ helper.boolean_badge(part.favorite) }} @@ -106,4 +111,4 @@ - \ No newline at end of file + diff --git a/templates/parts/info/_order_infos.html.twig b/templates/parts/info/_order_infos.html.twig index 68462de5..59b904df 100644 --- a/templates/parts/info/_order_infos.html.twig +++ b/templates/parts/info/_order_infos.html.twig @@ -24,8 +24,8 @@ {% if order.pricedetails is not empty %} - - +
+ @@ -36,32 +36,35 @@ {% endif %} - - - {% for detail in order.pricedetails %} - + + + {% for detail in order.pricedetails %} + {# @var detail App\Entity\PriceInformations\Pricedetail #} + - - - - - {% endfor %} - -
{% trans %}part.order.minamount{% endtrans %} {% trans %}part.order.price{% endtrans %}
- {{ detail.MinDiscountQuantity | format_amount(part.partUnit) }} - - {{ detail.price | format_money(detail.currency) }} / {{ detail.PriceRelatedQuantity | format_amount(part.partUnit) }} - {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency) %} - {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} - ({{ pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }}) - {% endif %} - - {{ detail.PricePerUnit | format_money(detail.currency) }} - {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency) %} - {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} - ({{ pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }}) - {% endif %} -
+ + {{ detail.MinDiscountQuantity | format_amount(part.partUnit) }} + + + {{ detail.price | format_money(detail.currency) }} / {{ detail.PriceRelatedQuantity | format_amount(part.partUnit) }} + {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency) %} + {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} + ({{ pricedetail_helper.convertMoneyToCurrency(detail.price, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }}) + {% endif %} + {{- helper.vat_text(detail.includesVAT) -}} + + + {{ detail.PricePerUnit | format_money(detail.currency) }} + {% set tmp = pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency) %} + {% if detail.currency != (app.user.currency ?? null) and tmp is not null and tmp.GreaterThan(0) %} + ({{ pricedetail_helper.convertMoneyToCurrency(detail.PricePerUnit, detail.currency, app.user.currency ?? null) | format_money(app.user.currency ?? null) }}) + {% endif %} + {{- helper.vat_text(detail.includesVAT) -}} + + + {% endfor %} + + {% endif %} {# Action for order information #} @@ -80,4 +83,4 @@ {% endfor %} - \ No newline at end of file + diff --git a/templates/parts/info/_part_lots.html.twig b/templates/parts/info/_part_lots.html.twig index 1ef25ae4..cfb7190b 100644 --- a/templates/parts/info/_part_lots.html.twig +++ b/templates/parts/info/_part_lots.html.twig @@ -2,6 +2,7 @@ {% import "label_system/dropdown_macro.html.twig" as dropdown %} {% include "parts/info/_withdraw_modal.html.twig" %} +{% include "parts/info/_stocktake_modal.html.twig" %}
@@ -19,53 +20,56 @@ {% for lot in part.partLots %} + {# @var lot App\Entity\Parts\PartLot #} - - - {% endfor %} diff --git a/templates/parts/info/_sidebar.html.twig b/templates/parts/info/_sidebar.html.twig index 0c353d8f..12060241 100644 --- a/templates/parts/info/_sidebar.html.twig +++ b/templates/parts/info/_sidebar.html.twig @@ -27,6 +27,14 @@ {% endif %} +{% if part.gtin %} +
+
+ {{ part.gtin }} +
+
+{% endif %} + {# Needs Review tag #} {% if part.needsReview %}
diff --git a/templates/parts/info/_stocktake_modal.html.twig b/templates/parts/info/_stocktake_modal.html.twig new file mode 100644 index 00000000..5e8c1ae5 --- /dev/null +++ b/templates/parts/info/_stocktake_modal.html.twig @@ -0,0 +1,63 @@ + diff --git a/templates/parts/lists/_filter.html.twig b/templates/parts/lists/_filter.html.twig index 2fb5bff2..3130f379 100644 --- a/templates/parts/lists/_filter.html.twig +++ b/templates/parts/lists/_filter.html.twig @@ -65,6 +65,7 @@ {{ form_row(filterForm.mass) }} {{ form_row(filterForm.dbId) }} {{ form_row(filterForm.ipn) }} + {{ form_row(filterForm.gtin) }} {{ form_row(filterForm.lastModified) }} {{ form_row(filterForm.addedDate) }}
@@ -163,4 +164,4 @@ {{ form_end(filterForm) }} - \ No newline at end of file + diff --git a/templates/parts/lists/_info_card.html.twig b/templates/parts/lists/_info_card.html.twig index 876bd31b..a35e2862 100644 --- a/templates/parts/lists/_info_card.html.twig +++ b/templates/parts/lists/_info_card.html.twig @@ -84,7 +84,7 @@ - {% if entity is instanceof("App\\Entity\\Parts\\Storelocation") %} + {% if entity is instanceof("App\\Entity\\Parts\\StorageLocation") %} {{ dropdown.profile_dropdown('storelocation', entity.id, true, 'btn-secondary w-100 mt-2') }} {% endif %} @@ -136,4 +136,4 @@ {% if filterForm is defined %} {% include "parts/lists/_filter.html.twig" %} {% endif %} - \ No newline at end of file + diff --git a/tests/API/APIDocsAvailabilityTest.php b/tests/API/APIDocsAvailabilityTest.php index b7caa873..a7bba3d6 100644 --- a/tests/API/APIDocsAvailabilityTest.php +++ b/tests/API/APIDocsAvailabilityTest.php @@ -28,7 +28,7 @@ use App\Entity\UserSystem\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class APIDocsAvailabilityTest extends WebTestCase +final class APIDocsAvailabilityTest extends WebTestCase { #[DataProvider('urlProvider')] public function testDocAvailabilityForLoggedInUser(string $url): void diff --git a/tests/API/APITokenAuthenticationTest.php b/tests/API/APITokenAuthenticationTest.php index a78b0594..803a1819 100644 --- a/tests/API/APITokenAuthenticationTest.php +++ b/tests/API/APITokenAuthenticationTest.php @@ -27,7 +27,7 @@ use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use App\DataFixtures\APITokenFixtures; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use ApiPlatform\Symfony\Bundle\Test\Client; -class APITokenAuthenticationTest extends ApiTestCase +final class APITokenAuthenticationTest extends ApiTestCase { public function testUnauthenticated(): void { diff --git a/tests/API/Endpoints/ApiTokenEnpointTest.php b/tests/API/Endpoints/ApiTokenEnpointTest.php index 99340182..f21716bd 100644 --- a/tests/API/Endpoints/ApiTokenEnpointTest.php +++ b/tests/API/Endpoints/ApiTokenEnpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\AuthenticatedApiTestCase; -class ApiTokenEnpointTest extends AuthenticatedApiTestCase +final class ApiTokenEnpointTest extends AuthenticatedApiTestCase { public function testGetCurrentToken(): void { diff --git a/tests/API/Endpoints/AttachmentTypeEndpointTest.php b/tests/API/Endpoints/AttachmentTypeEndpointTest.php index f90f3d94..fb5770d5 100644 --- a/tests/API/Endpoints/AttachmentTypeEndpointTest.php +++ b/tests/API/Endpoints/AttachmentTypeEndpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\Endpoints\CrudEndpointTestCase; -class AttachmentTypeEndpointTest extends CrudEndpointTestCase +final class AttachmentTypeEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/AttachmentsEndpointTest.php b/tests/API/Endpoints/AttachmentsEndpointTest.php index 8f4d7e77..999b7ad3 100644 --- a/tests/API/Endpoints/AttachmentsEndpointTest.php +++ b/tests/API/Endpoints/AttachmentsEndpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\AuthenticatedApiTestCase; -class AttachmentsEndpointTest extends AuthenticatedApiTestCase +final class AttachmentsEndpointTest extends AuthenticatedApiTestCase { public function testGetCollection(): void { diff --git a/tests/API/Endpoints/CategoryEndpointTest.php b/tests/API/Endpoints/CategoryEndpointTest.php index 68f4fd2d..5f54d1dd 100644 --- a/tests/API/Endpoints/CategoryEndpointTest.php +++ b/tests/API/Endpoints/CategoryEndpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\Endpoints\CrudEndpointTestCase; -class CategoryEndpointTest extends CrudEndpointTestCase +final class CategoryEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/CurrencyEndpointTest.php b/tests/API/Endpoints/CurrencyEndpointTest.php index a463daeb..a9f36633 100644 --- a/tests/API/Endpoints/CurrencyEndpointTest.php +++ b/tests/API/Endpoints/CurrencyEndpointTest.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace App\Tests\API\Endpoints; -class CurrencyEndpointTest extends CrudEndpointTestCase +final class CurrencyEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/FootprintsEndpointTest.php b/tests/API/Endpoints/FootprintsEndpointTest.php index f3f359a2..fd6374f9 100644 --- a/tests/API/Endpoints/FootprintsEndpointTest.php +++ b/tests/API/Endpoints/FootprintsEndpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\Endpoints\CrudEndpointTestCase; -class FootprintsEndpointTest extends CrudEndpointTestCase +final class FootprintsEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/InfoEndpointTest.php b/tests/API/Endpoints/InfoEndpointTest.php index 09f02e8a..6e996c0c 100644 --- a/tests/API/Endpoints/InfoEndpointTest.php +++ b/tests/API/Endpoints/InfoEndpointTest.php @@ -25,7 +25,7 @@ namespace API\Endpoints; use App\Tests\API\AuthenticatedApiTestCase; -class InfoEndpointTest extends AuthenticatedApiTestCase +final class InfoEndpointTest extends AuthenticatedApiTestCase { public function testGetInfo(): void { diff --git a/tests/API/Endpoints/LabelEndpointTest.php b/tests/API/Endpoints/LabelEndpointTest.php new file mode 100644 index 00000000..338af836 --- /dev/null +++ b/tests/API/Endpoints/LabelEndpointTest.php @@ -0,0 +1,186 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\API\Endpoints; + +use App\Tests\API\AuthenticatedApiTestCase; + +class LabelEndpointTest extends AuthenticatedApiTestCase +{ + public function testGetLabelProfiles(): void + { + $response = self::createAuthenticatedClient()->request('GET', '/api/label_profiles'); + + self::assertResponseIsSuccessful(); + self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); + + // Check that we get an array of label profiles + $json = $response->toArray(); + self::assertIsArray($json['hydra:member']); + self::assertNotEmpty($json['hydra:member']); + + // Check the structure of the first profile + $firstProfile = $json['hydra:member'][0]; + self::assertArrayHasKey('@id', $firstProfile); + self::assertArrayHasKey('name', $firstProfile); + self::assertArrayHasKey('options', $firstProfile); + self::assertArrayHasKey('show_in_dropdown', $firstProfile); + } + + public function testGetSingleLabelProfile(): void + { + $response = self::createAuthenticatedClient()->request('GET', '/api/label_profiles/1'); + + self::assertResponseIsSuccessful(); + self::assertJsonContains([ + '@id' => '/api/label_profiles/1', + ]); + + $json = $response->toArray(); + self::assertArrayHasKey('name', $json); + self::assertArrayHasKey('options', $json); + // Note: options is serialized but individual fields like width/height + // are only available in 'extended' or 'full' serialization groups + self::assertIsArray($json['options']); + } + + public function testFilterLabelProfilesByElementType(): void + { + $response = self::createAuthenticatedClient()->request('GET', '/api/label_profiles?options.supported_element=part'); + + self::assertResponseIsSuccessful(); + + $json = $response->toArray(); + // Check that we get results - the filter should work even if the field isn't in response + self::assertIsArray($json['hydra:member']); + // verify we got profiles + self::assertNotEmpty($json['hydra:member']); + } + + public function testGenerateLabelPdf(): void + { + $response = self::createAuthenticatedClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => '1', + ], + ]); + + self::assertResponseIsSuccessful(); + self::assertResponseHeaderSame('content-type', 'application/pdf'); + + // Check that the response contains PDF data + $content = $response->getContent(); + self::assertStringStartsWith('%PDF-', $content); + + // Check Content-Disposition header contains attachment and .pdf + $headers = $response->getHeaders(); + self::assertArrayHasKey('content-disposition', $headers); + $disposition = $headers['content-disposition'][0]; + self::assertStringContainsString('attachment', $disposition); + self::assertStringContainsString('.pdf', $disposition); + } + + public function testGenerateLabelPdfWithMultipleElements(): void + { + $response = self::createAuthenticatedClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => '1,2,3', + ], + ]); + + self::assertResponseIsSuccessful(); + self::assertResponseHeaderSame('content-type', 'application/pdf'); + self::assertStringStartsWith('%PDF-', $response->getContent()); + } + + public function testGenerateLabelPdfWithRange(): void + { + $response = self::createAuthenticatedClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => '1-3', + ], + ]); + + self::assertResponseIsSuccessful(); + self::assertResponseHeaderSame('content-type', 'application/pdf'); + self::assertStringStartsWith('%PDF-', $response->getContent()); + } + + public function testGenerateLabelPdfWithInvalidProfileId(): void + { + self::createAuthenticatedClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 99999, + 'elementIds' => '1', + ], + ]); + + self::assertResponseStatusCodeSame(404); + } + + public function testGenerateLabelPdfWithInvalidElementIds(): void + { + $client = self::createAuthenticatedClient(); + $client->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => 'invalid', + ], + ]); + + // Should return 400 or 422 (validation error) + $response = $client->getResponse(); + $statusCode = $response->getStatusCode(); + self::assertTrue( + $statusCode === 400 || $statusCode === 422, + "Expected status code 400 or 422, got {$statusCode}" + ); + } + + public function testGenerateLabelPdfWithNonExistentElements(): void + { + self::createAuthenticatedClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => '99999', + ], + ]); + + self::assertResponseStatusCodeSame(404); + } + + public function testGenerateLabelPdfRequiresAuthentication(): void + { + // Create a non-authenticated client + self::createClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => '1', + ], + ]); + + self::assertResponseStatusCodeSame(401); + } +} diff --git a/tests/API/Endpoints/ManufacturersEndpointTest.php b/tests/API/Endpoints/ManufacturersEndpointTest.php index 482ec98d..80447c93 100644 --- a/tests/API/Endpoints/ManufacturersEndpointTest.php +++ b/tests/API/Endpoints/ManufacturersEndpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\Endpoints\CrudEndpointTestCase; -class ManufacturersEndpointTest extends CrudEndpointTestCase +final class ManufacturersEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/MeasurementUnitsEndpointTest.php b/tests/API/Endpoints/MeasurementUnitsEndpointTest.php index db7341db..d659fb1c 100644 --- a/tests/API/Endpoints/MeasurementUnitsEndpointTest.php +++ b/tests/API/Endpoints/MeasurementUnitsEndpointTest.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace App\Tests\API\Endpoints; -class MeasurementUnitsEndpointTest extends CrudEndpointTestCase +final class MeasurementUnitsEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/OrderdetailsEndpointTest.php b/tests/API/Endpoints/OrderdetailsEndpointTest.php index 92823103..d7d132d9 100644 --- a/tests/API/Endpoints/OrderdetailsEndpointTest.php +++ b/tests/API/Endpoints/OrderdetailsEndpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\Endpoints\CrudEndpointTestCase; -class OrderdetailsEndpointTest extends CrudEndpointTestCase +final class OrderdetailsEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/ParametersEndpointTest.php b/tests/API/Endpoints/ParametersEndpointTest.php index 733df59a..323fc7b8 100644 --- a/tests/API/Endpoints/ParametersEndpointTest.php +++ b/tests/API/Endpoints/ParametersEndpointTest.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace App\Tests\API\Endpoints; -class ParametersEndpointTest extends CrudEndpointTestCase +final class ParametersEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/PartAssociationsEndpointTest.php b/tests/API/Endpoints/PartAssociationsEndpointTest.php index 62408dbb..7ac81ff6 100644 --- a/tests/API/Endpoints/PartAssociationsEndpointTest.php +++ b/tests/API/Endpoints/PartAssociationsEndpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\Endpoints\CrudEndpointTestCase; -class PartAssociationsEndpointTest extends CrudEndpointTestCase +final class PartAssociationsEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/PartCustomStateEndpointTest.php b/tests/API/Endpoints/PartCustomStateEndpointTest.php index ac353d9c..8d1253f3 100644 --- a/tests/API/Endpoints/PartCustomStateEndpointTest.php +++ b/tests/API/Endpoints/PartCustomStateEndpointTest.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace App\Tests\API\Endpoints; -class PartCustomStateEndpointTest extends CrudEndpointTestCase +final class PartCustomStateEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/PartEndpointTest.php b/tests/API/Endpoints/PartEndpointTest.php index 9406fc78..8d66d362 100644 --- a/tests/API/Endpoints/PartEndpointTest.php +++ b/tests/API/Endpoints/PartEndpointTest.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace App\Tests\API\Endpoints; -class PartEndpointTest extends CrudEndpointTestCase +final class PartEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/PartLotsEndpointTest.php b/tests/API/Endpoints/PartLotsEndpointTest.php index 38aa6b18..70f1f9ab 100644 --- a/tests/API/Endpoints/PartLotsEndpointTest.php +++ b/tests/API/Endpoints/PartLotsEndpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\Endpoints\CrudEndpointTestCase; -class PartLotsEndpointTest extends CrudEndpointTestCase +final class PartLotsEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/PricedetailsEndpointTest.php b/tests/API/Endpoints/PricedetailsEndpointTest.php index 8895365f..5661c0c7 100644 --- a/tests/API/Endpoints/PricedetailsEndpointTest.php +++ b/tests/API/Endpoints/PricedetailsEndpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\Endpoints\CrudEndpointTestCase; -class PricedetailsEndpointTest extends CrudEndpointTestCase +final class PricedetailsEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/ProjectBOMEntriesEndpointTest.php b/tests/API/Endpoints/ProjectBOMEntriesEndpointTest.php index cafb57dc..10dbf747 100644 --- a/tests/API/Endpoints/ProjectBOMEntriesEndpointTest.php +++ b/tests/API/Endpoints/ProjectBOMEntriesEndpointTest.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace App\Tests\API\Endpoints; -class ProjectBOMEntriesEndpointTest extends CrudEndpointTestCase +final class ProjectBOMEntriesEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/ProjectsEndpointTest.php b/tests/API/Endpoints/ProjectsEndpointTest.php index 9daf584a..ea9cc6b4 100644 --- a/tests/API/Endpoints/ProjectsEndpointTest.php +++ b/tests/API/Endpoints/ProjectsEndpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\Endpoints\CrudEndpointTestCase; -class ProjectsEndpointTest extends CrudEndpointTestCase +final class ProjectsEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/StorageLocationsEndpointTest.php b/tests/API/Endpoints/StorageLocationsEndpointTest.php index 8d9641c4..11947e71 100644 --- a/tests/API/Endpoints/StorageLocationsEndpointTest.php +++ b/tests/API/Endpoints/StorageLocationsEndpointTest.php @@ -25,7 +25,7 @@ namespace API\Endpoints; use App\Tests\API\Endpoints\CrudEndpointTestCase; -class StorageLocationsEndpointTest extends CrudEndpointTestCase +final class StorageLocationsEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/SuppliersEndpointTest.php b/tests/API/Endpoints/SuppliersEndpointTest.php index 1941f849..bbb64e90 100644 --- a/tests/API/Endpoints/SuppliersEndpointTest.php +++ b/tests/API/Endpoints/SuppliersEndpointTest.php @@ -25,7 +25,7 @@ namespace App\Tests\API\Endpoints; use App\Tests\API\Endpoints\CrudEndpointTestCase; -class SuppliersEndpointTest extends CrudEndpointTestCase +final class SuppliersEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/API/Endpoints/UsersEndpointTest.php b/tests/API/Endpoints/UsersEndpointTest.php index 0f075a7c..e6e18930 100644 --- a/tests/API/Endpoints/UsersEndpointTest.php +++ b/tests/API/Endpoints/UsersEndpointTest.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace App\Tests\API\Endpoints; -class UsersEndpointTest extends CrudEndpointTestCase +final class UsersEndpointTest extends CrudEndpointTestCase { protected function getBasePath(): string diff --git a/tests/ApplicationAvailabilityFunctionalTest.php b/tests/ApplicationAvailabilityFunctionalTest.php index d5bced49..c7449411 100644 --- a/tests/ApplicationAvailabilityFunctionalTest.php +++ b/tests/ApplicationAvailabilityFunctionalTest.php @@ -32,7 +32,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; */ #[Group('DB')] #[Group('slow')] -class ApplicationAvailabilityFunctionalTest extends WebTestCase +final class ApplicationAvailabilityFunctionalTest extends WebTestCase { #[DataProvider('urlProvider')] public function testPageIsSuccessful(string $url): void diff --git a/tests/Controller/AdminPages/AttachmentTypeController.php b/tests/Controller/AdminPages/AttachmentTypeController.php index 599a6f69..90e6583d 100644 --- a/tests/Controller/AdminPages/AttachmentTypeController.php +++ b/tests/Controller/AdminPages/AttachmentTypeController.php @@ -27,7 +27,7 @@ use App\Entity\Attachments\AttachmentType; #[Group('slow')] #[Group('DB')] -class AttachmentTypeController extends AbstractAdminController +final class AttachmentTypeController extends AbstractAdminController { protected static string $base_path = '/en/attachment_type'; protected static string $entity_class = AttachmentType::class; diff --git a/tests/Controller/AdminPages/CategoryController.php b/tests/Controller/AdminPages/CategoryController.php index c1bac093..5d8396e7 100644 --- a/tests/Controller/AdminPages/CategoryController.php +++ b/tests/Controller/AdminPages/CategoryController.php @@ -27,7 +27,7 @@ use App\Entity\Parts\Category; #[Group('slow')] #[Group('DB')] -class CategoryController extends AbstractAdminController +final class CategoryController extends AbstractAdminController { protected static string $base_path = '/en/category'; protected static string $entity_class = Category::class; diff --git a/tests/Controller/AdminPages/CurrencyController.php b/tests/Controller/AdminPages/CurrencyController.php index 21f94a29..4ebd82e2 100644 --- a/tests/Controller/AdminPages/CurrencyController.php +++ b/tests/Controller/AdminPages/CurrencyController.php @@ -28,7 +28,7 @@ use App\Entity\Parts\Manufacturer; #[Group('slow')] #[Group('DB')] -class CurrencyController extends AbstractAdminController +final class CurrencyController extends AbstractAdminController { protected static string $base_path = '/en/currency'; protected static string $entity_class = Currency::class; diff --git a/tests/Controller/AdminPages/FootprintController.php b/tests/Controller/AdminPages/FootprintController.php index 7d617ba8..2643d3f1 100644 --- a/tests/Controller/AdminPages/FootprintController.php +++ b/tests/Controller/AdminPages/FootprintController.php @@ -27,7 +27,7 @@ use App\Entity\Parts\Footprint; #[Group('slow')] #[Group('DB')] -class FootprintController extends AbstractAdminController +final class FootprintController extends AbstractAdminController { protected static string $base_path = '/en/footprint'; protected static string $entity_class = Footprint::class; diff --git a/tests/Controller/AdminPages/LabelProfileController.php b/tests/Controller/AdminPages/LabelProfileController.php index 838d872e..d407701a 100644 --- a/tests/Controller/AdminPages/LabelProfileController.php +++ b/tests/Controller/AdminPages/LabelProfileController.php @@ -46,7 +46,7 @@ use PHPUnit\Framework\Attributes\Group; use App\Entity\LabelSystem\LabelProfile; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -class LabelProfileController extends AbstractAdminController +final class LabelProfileController extends AbstractAdminController { protected static string $base_path = '/en/label_profile'; protected static string $entity_class = LabelProfile::class; diff --git a/tests/Controller/AdminPages/ManufacturerController.php b/tests/Controller/AdminPages/ManufacturerController.php index c2666f72..2a5ed386 100644 --- a/tests/Controller/AdminPages/ManufacturerController.php +++ b/tests/Controller/AdminPages/ManufacturerController.php @@ -27,7 +27,7 @@ use App\Entity\Parts\Manufacturer; #[Group('slow')] #[Group('DB')] -class ManufacturerController extends AbstractAdminController +final class ManufacturerController extends AbstractAdminController { protected static string $base_path = '/en/manufacturer'; protected static string $entity_class = Manufacturer::class; diff --git a/tests/Controller/AdminPages/MeasurementUnitController.php b/tests/Controller/AdminPages/MeasurementUnitController.php index 351f4e51..c15d4af4 100644 --- a/tests/Controller/AdminPages/MeasurementUnitController.php +++ b/tests/Controller/AdminPages/MeasurementUnitController.php @@ -27,7 +27,7 @@ use App\Entity\Parts\MeasurementUnit; #[Group('slow')] #[Group('DB')] -class MeasurementUnitController extends AbstractAdminController +final class MeasurementUnitController extends AbstractAdminController { protected static string $base_path = '/en/measurement_unit'; protected static string $entity_class = MeasurementUnit::class; diff --git a/tests/Controller/AdminPages/PartCustomStateControllerTest.php b/tests/Controller/AdminPages/PartCustomStateControllerTest.php index 3e87dfe2..77d1127c 100644 --- a/tests/Controller/AdminPages/PartCustomStateControllerTest.php +++ b/tests/Controller/AdminPages/PartCustomStateControllerTest.php @@ -27,7 +27,7 @@ use PHPUnit\Framework\Attributes\Group; #[Group('slow')] #[Group('DB')] -class PartCustomStateControllerTest extends AbstractAdminController +final class PartCustomStateControllerTest extends AbstractAdminController { protected static string $base_path = '/en/part_custom_state'; protected static string $entity_class = PartCustomState::class; diff --git a/tests/Controller/AdminPages/ProjectController.php b/tests/Controller/AdminPages/ProjectController.php index 1de4bf52..d7bec069 100644 --- a/tests/Controller/AdminPages/ProjectController.php +++ b/tests/Controller/AdminPages/ProjectController.php @@ -28,7 +28,7 @@ use App\Entity\ProjectSystem\Project; #[Group('slow')] #[Group('DB')] -class ProjectController extends AbstractAdminController +final class ProjectController extends AbstractAdminController { protected static string $base_path = '/en/project'; protected static string $entity_class = Project::class; diff --git a/tests/Controller/AdminPages/StorelocationController.php b/tests/Controller/AdminPages/StorelocationController.php index fee06c67..f19a5f6a 100644 --- a/tests/Controller/AdminPages/StorelocationController.php +++ b/tests/Controller/AdminPages/StorelocationController.php @@ -27,7 +27,7 @@ use App\Entity\Parts\StorageLocation; #[Group('slow')] #[Group('DB')] -class StorelocationController extends AbstractAdminController +final class StorelocationController extends AbstractAdminController { protected static string $base_path = '/en/store_location'; protected static string $entity_class = StorageLocation::class; diff --git a/tests/Controller/AdminPages/SupplierController.php b/tests/Controller/AdminPages/SupplierController.php index 3549eb4b..1e1d720a 100644 --- a/tests/Controller/AdminPages/SupplierController.php +++ b/tests/Controller/AdminPages/SupplierController.php @@ -27,7 +27,7 @@ use App\Entity\Parts\Supplier; #[Group('slow')] #[Group('DB')] -class SupplierController extends AbstractAdminController +final class SupplierController extends AbstractAdminController { protected static string $base_path = '/en/supplier'; protected static string $entity_class = Supplier::class; diff --git a/tests/Controller/BulkInfoProviderImportControllerTest.php b/tests/Controller/BulkInfoProviderImportControllerTest.php index 8961d23b..ec3629fe 100644 --- a/tests/Controller/BulkInfoProviderImportControllerTest.php +++ b/tests/Controller/BulkInfoProviderImportControllerTest.php @@ -22,6 +22,8 @@ declare(strict_types=1); namespace App\Tests\Controller; +use App\Services\InfoProviderSystem\BulkInfoProviderService; +use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO; use App\Entity\InfoProviderSystem\BulkImportJobStatus; use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob; use App\Entity\Parts\Part; @@ -36,7 +38,7 @@ use Symfony\Component\HttpFoundation\Response; #[Group("slow")] #[Group("DB")] -class BulkInfoProviderImportControllerTest extends WebTestCase +final class BulkInfoProviderImportControllerTest extends WebTestCase { public function testStep1WithoutIds(): void { @@ -174,8 +176,8 @@ class BulkInfoProviderImportControllerTest extends WebTestCase // Verify the template rendered the source_field and source_keyword correctly $content = $client->getResponse()->getContent(); - $this->assertStringContainsString('test_field', $content); - $this->assertStringContainsString('test_keyword', $content); + $this->assertStringContainsString('test_field', (string) $content); + $this->assertStringContainsString('test_keyword', (string) $content); // Clean up - find by ID to avoid detached entity issues $jobId = $job->getId(); @@ -607,7 +609,7 @@ class BulkInfoProviderImportControllerTest extends WebTestCase } $this->assertResponseStatusCodeSame(Response::HTTP_OK); - $this->assertStringContainsString('Bulk Info Provider Import', $client->getResponse()->getContent()); + $this->assertStringContainsString('Bulk Info Provider Import', (string) $client->getResponse()->getContent()); } public function testStep1FormSubmissionWithErrors(): void @@ -630,7 +632,7 @@ class BulkInfoProviderImportControllerTest extends WebTestCase } $this->assertResponseStatusCodeSame(Response::HTTP_OK); - $this->assertStringContainsString('Bulk Info Provider Import', $client->getResponse()->getContent()); + $this->assertStringContainsString('Bulk Info Provider Import', (string) $client->getResponse()->getContent()); } public function testBulkInfoProviderServiceKeywordExtraction(): void @@ -647,18 +649,18 @@ class BulkInfoProviderImportControllerTest extends WebTestCase } // Test that the service can extract keywords from parts - $bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class); + $bulkService = $client->getContainer()->get(BulkInfoProviderService::class); // Create field mappings to verify the service works $fieldMappings = [ - new \App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO('name', ['test'], 1), - new \App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO('mpn', ['test'], 2) + new BulkSearchFieldMappingDTO('name', ['test'], 1), + new BulkSearchFieldMappingDTO('mpn', ['test'], 2) ]; // The service may return an empty result or throw when no results are found try { $result = $bulkService->performBulkSearch([$part], $fieldMappings, false); - $this->assertInstanceOf(\App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO::class, $result); + $this->assertInstanceOf(BulkSearchResponseDTO::class, $result); } catch (\RuntimeException $e) { $this->assertStringContainsString('No search results found', $e->getMessage()); } @@ -725,12 +727,12 @@ class BulkInfoProviderImportControllerTest extends WebTestCase } // Test that the service can handle supplier part number fields - $bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class); + $bulkService = $client->getContainer()->get(BulkInfoProviderService::class); // Create field mappings with supplier SPN field mapping $fieldMappings = [ - new \App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO('invalid_field', ['test'], 1), - new \App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO('test_supplier_spn', ['test'], 2) + new BulkSearchFieldMappingDTO('invalid_field', ['test'], 1), + new BulkSearchFieldMappingDTO('test_supplier_spn', ['test'], 2) ]; // The service should be able to process the request and throw an exception when no results are found @@ -756,11 +758,11 @@ class BulkInfoProviderImportControllerTest extends WebTestCase } // Test that the service can handle batch processing - $bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class); + $bulkService = $client->getContainer()->get(BulkInfoProviderService::class); // Create field mappings with multiple keywords $fieldMappings = [ - new \App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO('empty', ['test'], 1) + new BulkSearchFieldMappingDTO('empty', ['test'], 1) ]; // The service should be able to process the request and throw an exception when no results are found @@ -786,7 +788,7 @@ class BulkInfoProviderImportControllerTest extends WebTestCase } // Test that the service can handle prefetch details - $bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class); + $bulkService = $client->getContainer()->get(BulkInfoProviderService::class); // Create empty search results to test prefetch method $searchResults = new BulkSearchResponseDTO([ diff --git a/tests/Controller/KiCadApiControllerTest.php b/tests/Controller/KiCadApiControllerTest.php index a66cb8a4..9d33512a 100644 --- a/tests/Controller/KiCadApiControllerTest.php +++ b/tests/Controller/KiCadApiControllerTest.php @@ -27,7 +27,7 @@ use App\DataFixtures\APITokenFixtures; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class KiCadApiControllerTest extends WebTestCase +final class KiCadApiControllerTest extends WebTestCase { private const BASE_URL = '/en/kicad-api/v1'; diff --git a/tests/Controller/PartControllerTest.php b/tests/Controller/PartControllerTest.php index 8c9f3729..c15bdd51 100644 --- a/tests/Controller/PartControllerTest.php +++ b/tests/Controller/PartControllerTest.php @@ -38,7 +38,7 @@ use Symfony\Component\HttpFoundation\Response; #[Group("slow")] #[Group("DB")] -class PartControllerTest extends WebTestCase +final class PartControllerTest extends WebTestCase { public function testShowPart(): void { diff --git a/tests/Controller/RedirectControllerTest.php b/tests/Controller/RedirectControllerTest.php index ac2776e5..420b0f49 100644 --- a/tests/Controller/RedirectControllerTest.php +++ b/tests/Controller/RedirectControllerTest.php @@ -33,7 +33,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; #[Group('slow')] #[Group('DB')] -class RedirectControllerTest extends WebTestCase +final class RedirectControllerTest extends WebTestCase { protected EntityManagerInterface $em; protected UserRepository $userRepo; diff --git a/tests/Controller/ScanControllerTest.php b/tests/Controller/ScanControllerTest.php index 98992e09..b504cd29 100644 --- a/tests/Controller/ScanControllerTest.php +++ b/tests/Controller/ScanControllerTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Controller; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class ScanControllerTest extends WebTestCase +final class ScanControllerTest extends WebTestCase { private ?KernelBrowser $client = null; diff --git a/tests/DataTables/Filters/CompoundFilterTraitTest.php b/tests/DataTables/Filters/CompoundFilterTraitTest.php index 93f3c1e1..d9bf20b0 100644 --- a/tests/DataTables/Filters/CompoundFilterTraitTest.php +++ b/tests/DataTables/Filters/CompoundFilterTraitTest.php @@ -27,7 +27,7 @@ use App\DataTables\Filters\FilterInterface; use Doctrine\ORM\QueryBuilder; use PHPUnit\Framework\TestCase; -class CompoundFilterTraitTest extends TestCase +final class CompoundFilterTraitTest extends TestCase { public function testFindAllChildFiltersEmpty(): void @@ -49,9 +49,9 @@ class CompoundFilterTraitTest extends TestCase public function testFindAllChildFilters(): void { - $f1 = $this->createMock(FilterInterface::class); - $f2 = $this->createMock(FilterInterface::class); - $f3 = $this->createMock(FilterInterface::class); + $f1 = $this->createStub(FilterInterface::class); + $f2 = $this->createStub(FilterInterface::class); + $f3 = $this->createStub(FilterInterface::class); $filter = new class($f1, $f2, $f3, null) { use CompoundFilterTrait; @@ -108,7 +108,7 @@ class CompoundFilterTraitTest extends TestCase } }; - $qb = $this->createMock(QueryBuilder::class); + $qb = $this->createStub(QueryBuilder::class); $filter->_applyAllChildFilters($qb); } diff --git a/tests/DataTables/Filters/Constraints/FilterTraitTest.php b/tests/DataTables/Filters/Constraints/FilterTraitTest.php index e1e459d5..a7493dcf 100644 --- a/tests/DataTables/Filters/Constraints/FilterTraitTest.php +++ b/tests/DataTables/Filters/Constraints/FilterTraitTest.php @@ -26,7 +26,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\DataTables\Filters\Constraints\FilterTrait; use PHPUnit\Framework\TestCase; -class FilterTraitTest extends TestCase +final class FilterTraitTest extends TestCase { use FilterTrait; diff --git a/tests/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraintTest.php b/tests/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraintTest.php index 816a8035..333b9af5 100644 --- a/tests/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraintTest.php +++ b/tests/DataTables/Filters/Constraints/Part/BulkImportJobStatusConstraintTest.php @@ -28,7 +28,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use PHPUnit\Framework\TestCase; -class BulkImportJobStatusConstraintTest extends TestCase +final class BulkImportJobStatusConstraintTest extends TestCase { private BulkImportJobStatusConstraint $constraint; private QueryBuilder $queryBuilder; @@ -46,7 +46,7 @@ class BulkImportJobStatusConstraintTest extends TestCase public function testConstructor(): void { - $this->assertEquals([], $this->constraint->getValue()); + $this->assertSame([], $this->constraint->getValue()); $this->assertEmpty($this->constraint->getOperator()); $this->assertFalse($this->constraint->isEnabled()); } @@ -56,7 +56,7 @@ class BulkImportJobStatusConstraintTest extends TestCase $values = ['pending', 'in_progress']; $this->constraint->setValue($values); - $this->assertEquals($values, $this->constraint->getValue()); + $this->assertSame($values, $this->constraint->getValue()); } public function testGetAndSetOperator(): void @@ -64,7 +64,7 @@ class BulkImportJobStatusConstraintTest extends TestCase $operator = 'ANY'; $this->constraint->setOperator($operator); - $this->assertEquals($operator, $this->constraint->getOperator()); + $this->assertSame($operator, $this->constraint->getOperator()); } public function testIsEnabledWithEmptyValues(): void diff --git a/tests/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraintTest.php b/tests/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraintTest.php index bc110eda..e2b37287 100644 --- a/tests/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraintTest.php +++ b/tests/DataTables/Filters/Constraints/Part/BulkImportPartStatusConstraintTest.php @@ -28,7 +28,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use PHPUnit\Framework\TestCase; -class BulkImportPartStatusConstraintTest extends TestCase +final class BulkImportPartStatusConstraintTest extends TestCase { private BulkImportPartStatusConstraint $constraint; private QueryBuilder $queryBuilder; @@ -46,7 +46,7 @@ class BulkImportPartStatusConstraintTest extends TestCase public function testConstructor(): void { - $this->assertEquals([], $this->constraint->getValue()); + $this->assertSame([], $this->constraint->getValue()); $this->assertEmpty($this->constraint->getOperator()); $this->assertFalse($this->constraint->isEnabled()); } @@ -56,7 +56,7 @@ class BulkImportPartStatusConstraintTest extends TestCase $values = ['pending', 'completed', 'skipped']; $this->constraint->setValue($values); - $this->assertEquals($values, $this->constraint->getValue()); + $this->assertSame($values, $this->constraint->getValue()); } public function testGetAndSetOperator(): void @@ -64,7 +64,7 @@ class BulkImportPartStatusConstraintTest extends TestCase $operator = 'ANY'; $this->constraint->setOperator($operator); - $this->assertEquals($operator, $this->constraint->getOperator()); + $this->assertSame($operator, $this->constraint->getOperator()); } public function testIsEnabledWithEmptyValues(): void @@ -294,6 +294,6 @@ class BulkImportPartStatusConstraintTest extends TestCase $this->constraint->apply($this->queryBuilder); - $this->assertEquals($statusValues, $this->constraint->getValue()); + $this->assertSame($statusValues, $this->constraint->getValue()); } } diff --git a/tests/DatatablesAvailabilityTest.php b/tests/DatatablesAvailabilityTest.php index dad61be3..1447da73 100644 --- a/tests/DatatablesAvailabilityTest.php +++ b/tests/DatatablesAvailabilityTest.php @@ -44,7 +44,7 @@ namespace App\Tests; use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class DatatablesAvailabilityTest extends WebTestCase +final class DatatablesAvailabilityTest extends WebTestCase { #[DataProvider('urlProvider')] public function testDataTable(string $url, ?array $ordering = null): void diff --git a/tests/Doctrine/SQLiteRegexMiddlewareTest.php b/tests/Doctrine/SQLiteRegexMiddlewareTest.php index 67410f76..aa5e4da1 100644 --- a/tests/Doctrine/SQLiteRegexMiddlewareTest.php +++ b/tests/Doctrine/SQLiteRegexMiddlewareTest.php @@ -26,7 +26,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\Doctrine\Middleware\SQLiteRegexExtensionMiddlewareDriver; use PHPUnit\Framework\TestCase; -class SQLiteRegexMiddlewareTest extends TestCase +final class SQLiteRegexMiddlewareTest extends TestCase { public static function regexpDataProvider(): \Generator diff --git a/tests/Entity/Attachments/AttachmentTest.php b/tests/Entity/Attachments/AttachmentTest.php index 35222d63..ca55424c 100644 --- a/tests/Entity/Attachments/AttachmentTest.php +++ b/tests/Entity/Attachments/AttachmentTest.php @@ -55,7 +55,7 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use ReflectionClass; -class AttachmentTest extends TestCase +final class AttachmentTest extends TestCase { public function testEmptyState(): void { diff --git a/tests/Entity/Attachments/AttachmentTypeTest.php b/tests/Entity/Attachments/AttachmentTypeTest.php index f9f781d8..c966d23f 100644 --- a/tests/Entity/Attachments/AttachmentTypeTest.php +++ b/tests/Entity/Attachments/AttachmentTypeTest.php @@ -23,10 +23,12 @@ declare(strict_types=1); namespace App\Tests\Entity\Attachments; use App\Entity\Attachments\AttachmentType; +use App\Entity\Attachments\PartAttachment; +use App\Entity\Attachments\UserAttachment; use Doctrine\Common\Collections\Collection; use PHPUnit\Framework\TestCase; -class AttachmentTypeTest extends TestCase +final class AttachmentTypeTest extends TestCase { public function testEmptyState(): void { @@ -34,4 +36,51 @@ class AttachmentTypeTest extends TestCase $this->assertInstanceOf(Collection::class, $attachment_type->getAttachmentsForType()); $this->assertEmpty($attachment_type->getFiletypeFilter()); } + + public function testSetAllowedTargets(): void + { + $attachmentType = new AttachmentType(); + + + $this->expectException(\InvalidArgumentException::class); + $attachmentType->setAllowedTargets(['target1', 'target2']); + } + + public function testGetSetAllowedTargets(): void + { + $attachmentType = new AttachmentType(); + + $attachmentType->setAllowedTargets([PartAttachment::class, UserAttachment::class]); + $this->assertSame([PartAttachment::class, UserAttachment::class], $attachmentType->getAllowedTargets()); + //Caching should also work + $this->assertSame([PartAttachment::class, UserAttachment::class], $attachmentType->getAllowedTargets()); + + //Setting null should reset the allowed targets + $attachmentType->setAllowedTargets(null); + $this->assertNull($attachmentType->getAllowedTargets()); + } + + public function testIsAllowedForTarget(): void + { + $attachmentType = new AttachmentType(); + + //By default, all targets should be allowed + $this->assertTrue($attachmentType->isAllowedForTarget(PartAttachment::class)); + $this->assertTrue($attachmentType->isAllowedForTarget(UserAttachment::class)); + + //Set specific allowed targets + $attachmentType->setAllowedTargets([PartAttachment::class]); + $this->assertTrue($attachmentType->isAllowedForTarget(PartAttachment::class)); + $this->assertFalse($attachmentType->isAllowedForTarget(UserAttachment::class)); + + //Set both targets + $attachmentType->setAllowedTargets([PartAttachment::class, UserAttachment::class]); + $this->assertTrue($attachmentType->isAllowedForTarget(PartAttachment::class)); + $this->assertTrue($attachmentType->isAllowedForTarget(UserAttachment::class)); + + //Reset allowed targets + $attachmentType->setAllowedTargets(null); + $this->assertTrue($attachmentType->isAllowedForTarget(PartAttachment::class)); + $this->assertTrue($attachmentType->isAllowedForTarget(UserAttachment::class)); + } } diff --git a/tests/Entity/Base/AbstractStructuralDBElementTest.php b/tests/Entity/Base/AbstractStructuralDBElementTest.php index 3f8157ad..90a7dee2 100644 --- a/tests/Entity/Base/AbstractStructuralDBElementTest.php +++ b/tests/Entity/Base/AbstractStructuralDBElementTest.php @@ -31,7 +31,7 @@ use PHPUnit\Framework\TestCase; * Test StructuralDBElement entities. * Note: Because StructuralDBElement is abstract we use AttachmentType here as a placeholder. */ -class AbstractStructuralDBElementTest extends TestCase +final class AbstractStructuralDBElementTest extends TestCase { protected AttachmentType $root; protected AttachmentType $child1; diff --git a/tests/Entity/BulkImportJobStatusTest.php b/tests/Entity/BulkImportJobStatusTest.php index e8b4a977..c38d62e2 100644 --- a/tests/Entity/BulkImportJobStatusTest.php +++ b/tests/Entity/BulkImportJobStatusTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Entity; use App\Entity\InfoProviderSystem\BulkImportJobStatus; use PHPUnit\Framework\TestCase; -class BulkImportJobStatusTest extends TestCase +final class BulkImportJobStatusTest extends TestCase { public function testEnumValues(): void { diff --git a/tests/Entity/BulkInfoProviderImportJobPartTest.php b/tests/Entity/BulkInfoProviderImportJobPartTest.php index dd9600dd..94b05637 100644 --- a/tests/Entity/BulkInfoProviderImportJobPartTest.php +++ b/tests/Entity/BulkInfoProviderImportJobPartTest.php @@ -28,32 +28,25 @@ use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart; use App\Entity\Parts\Part; use PHPUnit\Framework\TestCase; -class BulkInfoProviderImportJobPartTest extends TestCase +final class BulkInfoProviderImportJobPartTest extends TestCase { - private BulkInfoProviderImportJob $job; - private Part $part; private BulkInfoProviderImportJobPart $jobPart; protected function setUp(): void { - $this->job = $this->createMock(BulkInfoProviderImportJob::class); - $this->part = $this->createMock(Part::class); - - $this->jobPart = new BulkInfoProviderImportJobPart($this->job, $this->part); + $this->jobPart = new BulkInfoProviderImportJobPart($this->createStub(BulkInfoProviderImportJob::class), $this->createStub(Part::class)); } public function testConstructor(): void { - $this->assertSame($this->job, $this->jobPart->getJob()); - $this->assertSame($this->part, $this->jobPart->getPart()); - $this->assertEquals(BulkImportPartStatus::PENDING, $this->jobPart->getStatus()); + $this->assertSame(BulkImportPartStatus::PENDING, $this->jobPart->getStatus()); $this->assertNull($this->jobPart->getReason()); $this->assertNull($this->jobPart->getCompletedAt()); } public function testGetAndSetJob(): void { - $newJob = $this->createMock(BulkInfoProviderImportJob::class); + $newJob = $this->createStub(BulkInfoProviderImportJob::class); $result = $this->jobPart->setJob($newJob); @@ -63,7 +56,7 @@ class BulkInfoProviderImportJobPartTest extends TestCase public function testGetAndSetPart(): void { - $newPart = $this->createMock(Part::class); + $newPart = $this->createStub(Part::class); $result = $this->jobPart->setPart($newPart); @@ -76,7 +69,7 @@ class BulkInfoProviderImportJobPartTest extends TestCase $result = $this->jobPart->setStatus(BulkImportPartStatus::COMPLETED); $this->assertSame($this->jobPart, $result); - $this->assertEquals(BulkImportPartStatus::COMPLETED, $this->jobPart->getStatus()); + $this->assertSame(BulkImportPartStatus::COMPLETED, $this->jobPart->getStatus()); } public function testGetAndSetReason(): void @@ -86,7 +79,7 @@ class BulkInfoProviderImportJobPartTest extends TestCase $result = $this->jobPart->setReason($reason); $this->assertSame($this->jobPart, $result); - $this->assertEquals($reason, $this->jobPart->getReason()); + $this->assertSame($reason, $this->jobPart->getReason()); } public function testGetAndSetCompletedAt(): void @@ -108,7 +101,7 @@ class BulkInfoProviderImportJobPartTest extends TestCase $afterTime = new \DateTimeImmutable(); $this->assertSame($this->jobPart, $result); - $this->assertEquals(BulkImportPartStatus::COMPLETED, $this->jobPart->getStatus()); + $this->assertSame(BulkImportPartStatus::COMPLETED, $this->jobPart->getStatus()); $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt()); $this->assertGreaterThanOrEqual($beforeTime, $this->jobPart->getCompletedAt()); $this->assertLessThanOrEqual($afterTime, $this->jobPart->getCompletedAt()); @@ -124,8 +117,8 @@ class BulkInfoProviderImportJobPartTest extends TestCase $afterTime = new \DateTimeImmutable(); $this->assertSame($this->jobPart, $result); - $this->assertEquals(BulkImportPartStatus::SKIPPED, $this->jobPart->getStatus()); - $this->assertEquals($reason, $this->jobPart->getReason()); + $this->assertSame(BulkImportPartStatus::SKIPPED, $this->jobPart->getStatus()); + $this->assertSame($reason, $this->jobPart->getReason()); $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt()); $this->assertGreaterThanOrEqual($beforeTime, $this->jobPart->getCompletedAt()); $this->assertLessThanOrEqual($afterTime, $this->jobPart->getCompletedAt()); @@ -136,8 +129,8 @@ class BulkInfoProviderImportJobPartTest extends TestCase $result = $this->jobPart->markAsSkipped(); $this->assertSame($this->jobPart, $result); - $this->assertEquals(BulkImportPartStatus::SKIPPED, $this->jobPart->getStatus()); - $this->assertEquals('', $this->jobPart->getReason()); + $this->assertSame(BulkImportPartStatus::SKIPPED, $this->jobPart->getStatus()); + $this->assertSame('', $this->jobPart->getReason()); $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt()); } @@ -151,8 +144,8 @@ class BulkInfoProviderImportJobPartTest extends TestCase $afterTime = new \DateTimeImmutable(); $this->assertSame($this->jobPart, $result); - $this->assertEquals(BulkImportPartStatus::FAILED, $this->jobPart->getStatus()); - $this->assertEquals($reason, $this->jobPart->getReason()); + $this->assertSame(BulkImportPartStatus::FAILED, $this->jobPart->getStatus()); + $this->assertSame($reason, $this->jobPart->getReason()); $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt()); $this->assertGreaterThanOrEqual($beforeTime, $this->jobPart->getCompletedAt()); $this->assertLessThanOrEqual($afterTime, $this->jobPart->getCompletedAt()); @@ -163,8 +156,8 @@ class BulkInfoProviderImportJobPartTest extends TestCase $result = $this->jobPart->markAsFailed(); $this->assertSame($this->jobPart, $result); - $this->assertEquals(BulkImportPartStatus::FAILED, $this->jobPart->getStatus()); - $this->assertEquals('', $this->jobPart->getReason()); + $this->assertSame(BulkImportPartStatus::FAILED, $this->jobPart->getStatus()); + $this->assertSame('', $this->jobPart->getReason()); $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt()); } @@ -176,7 +169,7 @@ class BulkInfoProviderImportJobPartTest extends TestCase $result = $this->jobPart->markAsPending(); $this->assertSame($this->jobPart, $result); - $this->assertEquals(BulkImportPartStatus::PENDING, $this->jobPart->getStatus()); + $this->assertSame(BulkImportPartStatus::PENDING, $this->jobPart->getStatus()); $this->assertNull($this->jobPart->getReason()); $this->assertNull($this->jobPart->getCompletedAt()); } @@ -281,7 +274,7 @@ class BulkInfoProviderImportJobPartTest extends TestCase // After marking as skipped, should have reason and completion time $this->jobPart->markAsSkipped('Skipped reason'); - $this->assertEquals('Skipped reason', $this->jobPart->getReason()); + $this->assertSame('Skipped reason', $this->jobPart->getReason()); $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt()); // After marking as pending, reason and completion time should be cleared @@ -291,7 +284,7 @@ class BulkInfoProviderImportJobPartTest extends TestCase // After marking as failed, should have reason and completion time $this->jobPart->markAsFailed('Failed reason'); - $this->assertEquals('Failed reason', $this->jobPart->getReason()); + $this->assertSame('Failed reason', $this->jobPart->getReason()); $this->assertInstanceOf(\DateTimeImmutable::class, $this->jobPart->getCompletedAt()); // After marking as completed, should have completion time (reason may remain from previous state) diff --git a/tests/Entity/BulkInfoProviderImportJobTest.php b/tests/Entity/BulkInfoProviderImportJobTest.php index c9841ac4..d1a854cd 100644 --- a/tests/Entity/BulkInfoProviderImportJobTest.php +++ b/tests/Entity/BulkInfoProviderImportJobTest.php @@ -22,6 +22,8 @@ declare(strict_types=1); namespace App\Tests\Entity; +use App\Entity\Parts\Part; +use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO; use App\Entity\InfoProviderSystem\BulkImportJobStatus; use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob; use App\Entity\UserSystem\User; @@ -31,7 +33,7 @@ use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use PHPUnit\Framework\TestCase; -class BulkInfoProviderImportJobTest extends TestCase +final class BulkInfoProviderImportJobTest extends TestCase { private BulkInfoProviderImportJob $job; private User $user; @@ -45,9 +47,9 @@ class BulkInfoProviderImportJobTest extends TestCase $this->job->setCreatedBy($this->user); } - private function createMockPart(int $id): \App\Entity\Parts\Part + private function createMockPart(int $id): Part { - $part = $this->createMock(\App\Entity\Parts\Part::class); + $part = $this->createMock(Part::class); $part->method('getId')->willReturn($id); $part->method('getName')->willReturn("Test Part {$id}"); return $part; @@ -58,7 +60,7 @@ class BulkInfoProviderImportJobTest extends TestCase $job = new BulkInfoProviderImportJob(); $this->assertInstanceOf(\DateTimeImmutable::class, $job->getCreatedAt()); - $this->assertEquals(BulkImportJobStatus::PENDING, $job->getStatus()); + $this->assertSame(BulkImportJobStatus::PENDING, $job->getStatus()); $this->assertEmpty($job->getPartIds()); $this->assertEmpty($job->getFieldMappings()); $this->assertEmpty($job->getSearchResultsRaw()); @@ -70,14 +72,14 @@ class BulkInfoProviderImportJobTest extends TestCase public function testBasicGettersSetters(): void { $this->job->setName('Test Job'); - $this->assertEquals('Test Job', $this->job->getName()); + $this->assertSame('Test Job', $this->job->getName()); // Test with actual parts - this is what actually works $parts = [$this->createMockPart(1), $this->createMockPart(2), $this->createMockPart(3)]; foreach ($parts as $part) { $this->job->addPart($part); } - $this->assertEquals([1, 2, 3], $this->job->getPartIds()); + $this->assertSame([1, 2, 3], $this->job->getPartIds()); $fieldMappings = [new BulkSearchFieldMappingDTO(field: 'field1', providers: ['provider1', 'provider2'])]; $this->job->setFieldMappings($fieldMappings); @@ -98,24 +100,24 @@ class BulkInfoProviderImportJobTest extends TestCase $this->assertFalse($this->job->isStopped()); $this->job->markAsInProgress(); - $this->assertEquals(BulkImportJobStatus::IN_PROGRESS, $this->job->getStatus()); + $this->assertSame(BulkImportJobStatus::IN_PROGRESS, $this->job->getStatus()); $this->assertTrue($this->job->isInProgress()); $this->assertFalse($this->job->isPending()); $this->job->markAsCompleted(); - $this->assertEquals(BulkImportJobStatus::COMPLETED, $this->job->getStatus()); + $this->assertSame(BulkImportJobStatus::COMPLETED, $this->job->getStatus()); $this->assertTrue($this->job->isCompleted()); $this->assertNotNull($this->job->getCompletedAt()); $job2 = new BulkInfoProviderImportJob(); $job2->markAsFailed(); - $this->assertEquals(BulkImportJobStatus::FAILED, $job2->getStatus()); + $this->assertSame(BulkImportJobStatus::FAILED, $job2->getStatus()); $this->assertTrue($job2->isFailed()); $this->assertNotNull($job2->getCompletedAt()); $job3 = new BulkInfoProviderImportJob(); $job3->markAsStopped(); - $this->assertEquals(BulkImportJobStatus::STOPPED, $job3->getStatus()); + $this->assertSame(BulkImportJobStatus::STOPPED, $job3->getStatus()); $this->assertTrue($job3->isStopped()); $this->assertNotNull($job3->getCompletedAt()); } @@ -139,7 +141,7 @@ class BulkInfoProviderImportJobTest extends TestCase public function testPartCount(): void { - $this->assertEquals(0, $this->job->getPartCount()); + $this->assertSame(0, $this->job->getPartCount()); // Test with actual parts - setPartIds doesn't actually add parts $parts = [ @@ -152,31 +154,31 @@ class BulkInfoProviderImportJobTest extends TestCase foreach ($parts as $part) { $this->job->addPart($part); } - $this->assertEquals(5, $this->job->getPartCount()); + $this->assertSame(5, $this->job->getPartCount()); } public function testResultCount(): void { - $this->assertEquals(0, $this->job->getResultCount()); + $this->assertSame(0, $this->job->getResultCount()); $searchResults = new BulkSearchResponseDTO([ - new \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO( + new BulkSearchPartResultsDTO( part: $this->createMockPart(1), searchResults: [new BulkSearchPartResultDTO(searchResult: new SearchResultDTO(provider_key: 'dummy', provider_id: '1234', name: 'Part 1', description: 'A part'))] ), - new \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO( + new BulkSearchPartResultsDTO( part: $this->createMockPart(2), searchResults: [new BulkSearchPartResultDTO(searchResult: new SearchResultDTO(provider_key: 'dummy', provider_id: '1234', name: 'Part 2', description: 'A part')), new BulkSearchPartResultDTO(searchResult: new SearchResultDTO(provider_key: 'dummy', provider_id: '5678', name: 'Part 2 Alt', description: 'Another part'))] ), - new \App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO( + new BulkSearchPartResultsDTO( part: $this->createMockPart(3), searchResults: [] ) ]); $this->job->setSearchResults($searchResults); - $this->assertEquals(3, $this->job->getResultCount()); + $this->assertSame(3, $this->job->getResultCount()); } public function testPartProgressTracking(): void @@ -222,21 +224,21 @@ class BulkInfoProviderImportJobTest extends TestCase $this->job->addPart($part); } - $this->assertEquals(0, $this->job->getCompletedPartsCount()); - $this->assertEquals(0, $this->job->getSkippedPartsCount()); + $this->assertSame(0, $this->job->getCompletedPartsCount()); + $this->assertSame(0, $this->job->getSkippedPartsCount()); $this->job->markPartAsCompleted(1); $this->job->markPartAsCompleted(2); $this->job->markPartAsSkipped(3, 'Error'); - $this->assertEquals(2, $this->job->getCompletedPartsCount()); - $this->assertEquals(1, $this->job->getSkippedPartsCount()); + $this->assertSame(2, $this->job->getCompletedPartsCount()); + $this->assertSame(1, $this->job->getSkippedPartsCount()); } public function testProgressPercentage(): void { $emptyJob = new BulkInfoProviderImportJob(); - $this->assertEquals(100.0, $emptyJob->getProgressPercentage()); + $this->assertEqualsWithDelta(100.0, $emptyJob->getProgressPercentage(), PHP_FLOAT_EPSILON); // Test with actual parts - setPartIds doesn't actually add parts $parts = [ @@ -250,18 +252,18 @@ class BulkInfoProviderImportJobTest extends TestCase $this->job->addPart($part); } - $this->assertEquals(0.0, $this->job->getProgressPercentage()); + $this->assertEqualsWithDelta(0.0, $this->job->getProgressPercentage(), PHP_FLOAT_EPSILON); $this->job->markPartAsCompleted(1); $this->job->markPartAsCompleted(2); - $this->assertEquals(40.0, $this->job->getProgressPercentage()); + $this->assertEqualsWithDelta(40.0, $this->job->getProgressPercentage(), PHP_FLOAT_EPSILON); $this->job->markPartAsSkipped(3, 'Error'); - $this->assertEquals(60.0, $this->job->getProgressPercentage()); + $this->assertEqualsWithDelta(60.0, $this->job->getProgressPercentage(), PHP_FLOAT_EPSILON); $this->job->markPartAsCompleted(4); $this->job->markPartAsCompleted(5); - $this->assertEquals(100.0, $this->job->getProgressPercentage()); + $this->assertEqualsWithDelta(100.0, $this->job->getProgressPercentage(), PHP_FLOAT_EPSILON); } public function testIsAllPartsCompleted(): void @@ -301,8 +303,8 @@ class BulkInfoProviderImportJobTest extends TestCase $this->job->addPart($part); } - $this->assertEquals('info_providers.bulk_import.job_name_template', $this->job->getDisplayNameKey()); - $this->assertEquals(['%count%' => 3], $this->job->getDisplayNameParams()); + $this->assertSame('info_providers.bulk_import.job_name_template', $this->job->getDisplayNameKey()); + $this->assertSame(['%count%' => 3], $this->job->getDisplayNameParams()); } public function testFormattedTimestamp(): void diff --git a/tests/Entity/LogSystem/AbstractLogEntryTest.php b/tests/Entity/LogSystem/AbstractLogEntryTest.php index 3f223693..d99b8e73 100644 --- a/tests/Entity/LogSystem/AbstractLogEntryTest.php +++ b/tests/Entity/LogSystem/AbstractLogEntryTest.php @@ -58,7 +58,7 @@ use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use PHPUnit\Framework\TestCase; -class AbstractLogEntryTest extends TestCase +final class AbstractLogEntryTest extends TestCase { public function testSetGetTarget(): void { diff --git a/tests/Entity/LogSystem/LogLevelTest.php b/tests/Entity/LogSystem/LogLevelTest.php index 402942e1..0125b0cd 100644 --- a/tests/Entity/LogSystem/LogLevelTest.php +++ b/tests/Entity/LogSystem/LogLevelTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Entity\LogSystem; use App\Entity\LogSystem\LogLevel; use PHPUnit\Framework\TestCase; -class LogLevelTest extends TestCase +final class LogLevelTest extends TestCase { public function testToPSR3LevelString(): void diff --git a/tests/Entity/LogSystem/LogTargetTypeTest.php b/tests/Entity/LogSystem/LogTargetTypeTest.php index 46682496..06e2ead1 100644 --- a/tests/Entity/LogSystem/LogTargetTypeTest.php +++ b/tests/Entity/LogSystem/LogTargetTypeTest.php @@ -30,7 +30,7 @@ use App\Entity\Parts\Category; use App\Entity\UserSystem\User; use PHPUnit\Framework\TestCase; -class LogTargetTypeTest extends TestCase +final class LogTargetTypeTest extends TestCase { public function testToClass(): void diff --git a/tests/Entity/Parameters/PartParameterTest.php b/tests/Entity/Parameters/PartParameterTest.php index 64550eee..6a07468e 100644 --- a/tests/Entity/Parameters/PartParameterTest.php +++ b/tests/Entity/Parameters/PartParameterTest.php @@ -45,7 +45,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\Entity\Parameters\PartParameter; use PHPUnit\Framework\TestCase; -class PartParameterTest extends TestCase +final class PartParameterTest extends TestCase { public static function valueWithUnitDataProvider(): \Iterator { diff --git a/tests/Entity/Parts/InfoProviderReferenceTest.php b/tests/Entity/Parts/InfoProviderReferenceTest.php index a1a8d5de..dcc6a43c 100644 --- a/tests/Entity/Parts/InfoProviderReferenceTest.php +++ b/tests/Entity/Parts/InfoProviderReferenceTest.php @@ -26,7 +26,7 @@ use App\Entity\Parts\InfoProviderReference; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use PHPUnit\Framework\TestCase; -class InfoProviderReferenceTest extends TestCase +final class InfoProviderReferenceTest extends TestCase { public function testNoProvider(): void { diff --git a/tests/Entity/Parts/PartAssociationTest.php b/tests/Entity/Parts/PartAssociationTest.php index e002846e..25487d1f 100644 --- a/tests/Entity/Parts/PartAssociationTest.php +++ b/tests/Entity/Parts/PartAssociationTest.php @@ -26,7 +26,7 @@ use App\Entity\Parts\AssociationType; use App\Entity\Parts\PartAssociation; use PHPUnit\Framework\TestCase; -class PartAssociationTest extends TestCase +final class PartAssociationTest extends TestCase { public function testGetTypeTranslationKey(): void diff --git a/tests/Entity/Parts/PartLotTest.php b/tests/Entity/Parts/PartLotTest.php index 30687b72..10cc80b9 100644 --- a/tests/Entity/Parts/PartLotTest.php +++ b/tests/Entity/Parts/PartLotTest.php @@ -26,7 +26,7 @@ use App\Entity\Parts\PartLot; use DateTime; use PHPUnit\Framework\TestCase; -class PartLotTest extends TestCase +final class PartLotTest extends TestCase { public function testIsExpired(): void { diff --git a/tests/Entity/Parts/PartTest.php b/tests/Entity/Parts/PartTest.php index c1ae8935..e855c340 100644 --- a/tests/Entity/Parts/PartTest.php +++ b/tests/Entity/Parts/PartTest.php @@ -29,7 +29,7 @@ use DateTime; use Doctrine\Common\Collections\Collection; use PHPUnit\Framework\TestCase; -class PartTest extends TestCase +final class PartTest extends TestCase { public function testAddRemovePartLot(): void { diff --git a/tests/Entity/PriceSystem/CurrencyTest.php b/tests/Entity/PriceSystem/CurrencyTest.php index 0058d501..018092e5 100644 --- a/tests/Entity/PriceSystem/CurrencyTest.php +++ b/tests/Entity/PriceSystem/CurrencyTest.php @@ -26,7 +26,7 @@ use App\Entity\PriceInformations\Currency; use Brick\Math\BigDecimal; use PHPUnit\Framework\TestCase; -class CurrencyTest extends TestCase +final class CurrencyTest extends TestCase { public function testGetInverseExchangeRate(): void { diff --git a/tests/Entity/PriceSystem/OrderdetailTest.php b/tests/Entity/PriceSystem/OrderdetailTest.php index 497f9ab3..2becb74e 100644 --- a/tests/Entity/PriceSystem/OrderdetailTest.php +++ b/tests/Entity/PriceSystem/OrderdetailTest.php @@ -27,7 +27,7 @@ use App\Entity\PriceInformations\Pricedetail; use Doctrine\Common\Collections\Collection; use PHPUnit\Framework\TestCase; -class OrderdetailTest extends TestCase +final class OrderdetailTest extends TestCase { public function testAddRemovePricdetails(): void { @@ -61,4 +61,18 @@ class OrderdetailTest extends TestCase $this->assertSame($price5, $orderdetail->findPriceForQty(5.3)); $this->assertSame($price5, $orderdetail->findPriceForQty(10000)); } + + public function testGetSetPricesIncludesVAT(): void + { + $orderdetail = new Orderdetail(); + + //By default, the pricesIncludesVAT property should be null for empty orderdetails + $this->assertNull($orderdetail->getPricesIncludesVAT()); + + $orderdetail->setPricesIncludesVAT(true); + $this->assertTrue($orderdetail->getPricesIncludesVAT()); + + $orderdetail->setPricesIncludesVAT(false); + $this->assertFalse($orderdetail->getPricesIncludesVAT()); + } } diff --git a/tests/Entity/PriceSystem/PricedetailTest.php b/tests/Entity/PriceSystem/PricedetailTest.php index 8a3cf328..effe6fd6 100644 --- a/tests/Entity/PriceSystem/PricedetailTest.php +++ b/tests/Entity/PriceSystem/PricedetailTest.php @@ -28,7 +28,7 @@ use App\Entity\PriceInformations\Pricedetail; use Brick\Math\BigDecimal; use PHPUnit\Framework\TestCase; -class PricedetailTest extends TestCase +final class PricedetailTest extends TestCase { public function testGetPricePerUnit(): void { diff --git a/tests/Entity/UserSystem/ApiTokenTypeTest.php b/tests/Entity/UserSystem/ApiTokenTypeTest.php index a8e520f1..7a4506ba 100644 --- a/tests/Entity/UserSystem/ApiTokenTypeTest.php +++ b/tests/Entity/UserSystem/ApiTokenTypeTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Entity\UserSystem; use App\Entity\UserSystem\ApiTokenType; use PHPUnit\Framework\TestCase; -class ApiTokenTypeTest extends TestCase +final class ApiTokenTypeTest extends TestCase { public function testGetTokenPrefix(): void diff --git a/tests/Entity/UserSystem/PermissionDataTest.php b/tests/Entity/UserSystem/PermissionDataTest.php index 4fd8c5ce..3d250a81 100644 --- a/tests/Entity/UserSystem/PermissionDataTest.php +++ b/tests/Entity/UserSystem/PermissionDataTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Entity\UserSystem; use App\Entity\UserSystem\PermissionData; use PHPUnit\Framework\TestCase; -class PermissionDataTest extends TestCase +final class PermissionDataTest extends TestCase { public function testGetSetIs(): void diff --git a/tests/Entity/UserSystem/UserTest.php b/tests/Entity/UserSystem/UserTest.php index a4349e1d..12797ad1 100644 --- a/tests/Entity/UserSystem/UserTest.php +++ b/tests/Entity/UserSystem/UserTest.php @@ -31,7 +31,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Uid\Uuid; use Webauthn\TrustPath\EmptyTrustPath; -class UserTest extends TestCase +final class UserTest extends TestCase { public function testGetFullName(): void { diff --git a/tests/EnvVarProcessors/AddSlashEnvVarProcessorTest.php b/tests/EnvVarProcessors/AddSlashEnvVarProcessorTest.php index 4099f0ee..c4c9f04b 100644 --- a/tests/EnvVarProcessors/AddSlashEnvVarProcessorTest.php +++ b/tests/EnvVarProcessors/AddSlashEnvVarProcessorTest.php @@ -1,4 +1,7 @@ . */ - namespace App\Tests\EnvVarProcessors; use App\EnvVarProcessors\AddSlashEnvVarProcessor; use PHPUnit\Framework\TestCase; -class AddSlashEnvVarProcessorTest extends TestCase +final class AddSlashEnvVarProcessorTest extends TestCase { protected AddSlashEnvVarProcessor $processor; diff --git a/tests/EventListener/RegisterSynonymsAsTranslationParametersTest.php b/tests/EventListener/RegisterSynonymsAsTranslationParametersTest.php index 58573ae6..3a35c670 100644 --- a/tests/EventListener/RegisterSynonymsAsTranslationParametersTest.php +++ b/tests/EventListener/RegisterSynonymsAsTranslationParametersTest.php @@ -1,4 +1,7 @@ . */ - namespace App\Tests\EventListener; use App\EventListener\RegisterSynonymsAsTranslationParametersListener; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -class RegisterSynonymsAsTranslationParametersTest extends KernelTestCase +final class RegisterSynonymsAsTranslationParametersTest extends KernelTestCase { private RegisterSynonymsAsTranslationParametersListener $listener; diff --git a/tests/EventSubscriber/PasswordChangeNeededSubscriberTest.php b/tests/EventSubscriber/PasswordChangeNeededSubscriberTest.php index 0eaf931c..3d2089e1 100644 --- a/tests/EventSubscriber/PasswordChangeNeededSubscriberTest.php +++ b/tests/EventSubscriber/PasswordChangeNeededSubscriberTest.php @@ -33,7 +33,7 @@ use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\Uid\Uuid; use Webauthn\TrustPath\EmptyTrustPath; -class PasswordChangeNeededSubscriberTest extends TestCase +final class PasswordChangeNeededSubscriberTest extends TestCase { public function testTFARedirectNeeded(): void { diff --git a/tests/Exceptions/TwigModeExceptionTest.php b/tests/Exceptions/TwigModeExceptionTest.php index 686a87a2..09468291 100644 --- a/tests/Exceptions/TwigModeExceptionTest.php +++ b/tests/Exceptions/TwigModeExceptionTest.php @@ -27,7 +27,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Twig\Error\Error; -class TwigModeExceptionTest extends KernelTestCase +final class TwigModeExceptionTest extends KernelTestCase { private string $projectPath; diff --git a/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php b/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php index 89e362e4..07106505 100644 --- a/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php +++ b/tests/Form/InfoProviderSystem/GlobalFieldMappingTypeTest.php @@ -29,7 +29,7 @@ use Symfony\Component\Form\FormFactoryInterface; #[Group("slow")] #[Group("DB")] -class GlobalFieldMappingTypeTest extends KernelTestCase +final class GlobalFieldMappingTypeTest extends KernelTestCase { private FormFactoryInterface $formFactory; diff --git a/tests/Helpers/BBCodeToMarkdownConverterTest.php b/tests/Helpers/BBCodeToMarkdownConverterTest.php index 9506cba4..07fca505 100644 --- a/tests/Helpers/BBCodeToMarkdownConverterTest.php +++ b/tests/Helpers/BBCodeToMarkdownConverterTest.php @@ -26,7 +26,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\Helpers\BBCodeToMarkdownConverter; use PHPUnit\Framework\TestCase; -class BBCodeToMarkdownConverterTest extends TestCase +final class BBCodeToMarkdownConverterTest extends TestCase { protected BBCodeToMarkdownConverter $converter; diff --git a/tests/Helpers/IPAnonymizerTest.php b/tests/Helpers/IPAnonymizerTest.php index e16368eb..7efd27ac 100644 --- a/tests/Helpers/IPAnonymizerTest.php +++ b/tests/Helpers/IPAnonymizerTest.php @@ -26,7 +26,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\Helpers\IPAnonymizer; use PHPUnit\Framework\TestCase; -class IPAnonymizerTest extends TestCase +final class IPAnonymizerTest extends TestCase { public static function anonymizeDataProvider(): \Generator diff --git a/tests/Helpers/Projects/ProjectBuildRequestTest.php b/tests/Helpers/Projects/ProjectBuildRequestTest.php index 1158d89a..c1fd1498 100644 --- a/tests/Helpers/Projects/ProjectBuildRequestTest.php +++ b/tests/Helpers/Projects/ProjectBuildRequestTest.php @@ -30,7 +30,7 @@ use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Helpers\Projects\ProjectBuildRequest; use PHPUnit\Framework\TestCase; -class ProjectBuildRequestTest extends TestCase +final class ProjectBuildRequestTest extends TestCase { /** @var Project */ diff --git a/tests/Helpers/TreeViewNodeTest.php b/tests/Helpers/TreeViewNodeTest.php index 9005651d..b1179f6c 100644 --- a/tests/Helpers/TreeViewNodeTest.php +++ b/tests/Helpers/TreeViewNodeTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Helpers; use App\Helpers\Trees\TreeViewNode; use PHPUnit\Framework\TestCase; -class TreeViewNodeTest extends TestCase +final class TreeViewNodeTest extends TestCase { /** * @var TreeViewNode diff --git a/tests/Helpers/TrinaryLogicHelperTest.php b/tests/Helpers/TrinaryLogicHelperTest.php index 3082571b..4b8c9f01 100644 --- a/tests/Helpers/TrinaryLogicHelperTest.php +++ b/tests/Helpers/TrinaryLogicHelperTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Helpers; use App\Helpers\TrinaryLogicHelper; use PHPUnit\Framework\TestCase; -class TrinaryLogicHelperTest extends TestCase +final class TrinaryLogicHelperTest extends TestCase { public function testNot() diff --git a/tests/Repository/AttachmentContainingDBElementRepositoryTest.php b/tests/Repository/AttachmentContainingDBElementRepositoryTest.php index f61750d9..38aca071 100644 --- a/tests/Repository/AttachmentContainingDBElementRepositoryTest.php +++ b/tests/Repository/AttachmentContainingDBElementRepositoryTest.php @@ -28,7 +28,7 @@ use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -class AttachmentContainingDBElementRepositoryTest extends KernelTestCase +final class AttachmentContainingDBElementRepositoryTest extends KernelTestCase { private EntityManagerInterface $entityManager; diff --git a/tests/Repository/DBElementRepositoryTest.php b/tests/Repository/DBElementRepositoryTest.php index 05ede7e2..5f1ac0e1 100644 --- a/tests/Repository/DBElementRepositoryTest.php +++ b/tests/Repository/DBElementRepositoryTest.php @@ -33,7 +33,7 @@ use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -class DBElementRepositoryTest extends KernelTestCase +final class DBElementRepositoryTest extends KernelTestCase { private EntityManagerInterface $entityManager; diff --git a/tests/Repository/LogEntryRepositoryTest.php b/tests/Repository/LogEntryRepositoryTest.php index f6cc991d..46093a9e 100644 --- a/tests/Repository/LogEntryRepositoryTest.php +++ b/tests/Repository/LogEntryRepositoryTest.php @@ -33,7 +33,7 @@ use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -class LogEntryRepositoryTest extends KernelTestCase +final class LogEntryRepositoryTest extends KernelTestCase { private EntityManagerInterface $entityManager; @@ -75,6 +75,7 @@ class LogEntryRepositoryTest extends KernelTestCase //We have a edit log entry for the category with ID 1 $category = $this->entityManager->find(Category::class, 1); $adminUser = $this->entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); + $this->assertInstanceOf(Category::class, $category); $user = $this->repo->getLastEditingUser($category); @@ -83,6 +84,7 @@ class LogEntryRepositoryTest extends KernelTestCase //For the category 2, the user must be null $category = $this->entityManager->find(Category::class, 2); + $this->assertInstanceOf(Category::class, $category); $user = $this->repo->getLastEditingUser($category); $this->assertNull($user); } @@ -92,6 +94,7 @@ class LogEntryRepositoryTest extends KernelTestCase //We have a edit log entry for the category with ID 1 $category = $this->entityManager->find(Category::class, 1); $adminUser = $this->entityManager->getRepository(User::class)->findOneBy(['name' => 'admin']); + $this->assertInstanceOf(Category::class, $category); $user = $this->repo->getCreatingUser($category); @@ -100,6 +103,7 @@ class LogEntryRepositoryTest extends KernelTestCase //For the category 2, the user must be null $category = $this->entityManager->find(Category::class, 2); + $this->assertInstanceOf(Category::class, $category); $user = $this->repo->getCreatingUser($category); $this->assertNull($user); } @@ -119,6 +123,7 @@ class LogEntryRepositoryTest extends KernelTestCase public function testGetElementExistedAtTimestamp(): void { $part = $this->entityManager->find(Part::class, 3); + $this->assertInstanceOf(Part::class, $part); //Assume that the part is existing now $this->assertTrue($this->repo->getElementExistedAtTimestamp($part, new \DateTimeImmutable())); @@ -130,6 +135,7 @@ class LogEntryRepositoryTest extends KernelTestCase public function testGetElementHistory(): void { $category = $this->entityManager->find(Category::class, 1); + $this->assertInstanceOf(Category::class, $category); $history = $this->repo->getElementHistory($category); @@ -141,6 +147,7 @@ class LogEntryRepositoryTest extends KernelTestCase public function testGetTimetravelDataForElement(): void { $category = $this->entityManager->find(Category::class, 1); + $this->assertInstanceOf(Category::class, $category); $data = $this->repo->getTimetravelDataForElement($category, new \DateTimeImmutable('2020-01-01')); //The data must contain only ElementChangedLogEntry diff --git a/tests/Repository/NamedDBElementRepositoryTest.php b/tests/Repository/NamedDBElementRepositoryTest.php index 117d7d0e..dc8b2a5c 100644 --- a/tests/Repository/NamedDBElementRepositoryTest.php +++ b/tests/Repository/NamedDBElementRepositoryTest.php @@ -30,7 +30,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; /** * @Group DB */ -class NamedDBElementRepositoryTest extends WebTestCase +final class NamedDBElementRepositoryTest extends WebTestCase { /** * @var StructuralDBElementRepository diff --git a/tests/Repository/PartRepositoryTest.php b/tests/Repository/PartRepositoryTest.php index 68b75abb..c2e7858a 100644 --- a/tests/Repository/PartRepositoryTest.php +++ b/tests/Repository/PartRepositoryTest.php @@ -60,8 +60,8 @@ final class PartRepositoryTest extends TestCase $classMetadata = new ClassMetadata(Part::class); $emMock->method('getClassMetadata')->with(Part::class)->willReturn($classMetadata); - $translatorMock = $this->createMock(TranslatorInterface::class); - $ipnSuggestSettings = $this->createMock(IpnSuggestSettings::class); + $translatorMock = $this->createStub(TranslatorInterface::class); + $ipnSuggestSettings = $this->createStub(IpnSuggestSettings::class); $repo = $this->getMockBuilder(PartRepository::class) ->setConstructorArgs([$emMock, $translatorMock, $ipnSuggestSettings]) @@ -120,7 +120,7 @@ final class PartRepositoryTest extends TestCase return $id; }); - $ipnSuggestSettings = $this->createMock(IpnSuggestSettings::class); + $ipnSuggestSettings = $this->createStub(IpnSuggestSettings::class); $ipnSuggestSettings->suggestPartDigits = 4; $ipnSuggestSettings->useDuplicateDescription = false; @@ -204,7 +204,7 @@ final class PartRepositoryTest extends TestCase return $id; }); - $ipnSuggestSettings = $this->createMock(IpnSuggestSettings::class); + $ipnSuggestSettings = $this->createStub(IpnSuggestSettings::class); $ipnSuggestSettings->suggestPartDigits = 4; $ipnSuggestSettings->useDuplicateDescription = false; diff --git a/tests/Repository/StructuralDBElementRepositoryTest.php b/tests/Repository/StructuralDBElementRepositoryTest.php index 5ab8b788..16e92837 100644 --- a/tests/Repository/StructuralDBElementRepositoryTest.php +++ b/tests/Repository/StructuralDBElementRepositoryTest.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace App\Tests\Repository; +use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Attachments\AttachmentType; use App\Helpers\Trees\TreeViewNode; use App\Repository\StructuralDBElementRepository; @@ -30,7 +31,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; /** * @Group DB */ -class StructuralDBElementRepositoryTest extends WebTestCase +final class StructuralDBElementRepositoryTest extends WebTestCase { /** * @var StructuralDBElementRepository @@ -108,6 +109,7 @@ class StructuralDBElementRepositoryTest extends WebTestCase { //List all nodes that are children to Node 1 $node1 = $this->repo->find(1); + $this->assertInstanceOf(AbstractStructuralDBElement::class, $node1); $nodes = $this->repo->getFlatList($node1); $this->assertCount(3, $nodes); diff --git a/tests/Repository/UserRepositoryTest.php b/tests/Repository/UserRepositoryTest.php index 67a77aea..24a2d657 100644 --- a/tests/Repository/UserRepositoryTest.php +++ b/tests/Repository/UserRepositoryTest.php @@ -27,7 +27,7 @@ use App\Repository\UserRepository; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class UserRepositoryTest extends WebTestCase +final class UserRepositoryTest extends WebTestCase { /** diff --git a/tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php b/tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php index c9a14426..2fedf108 100644 --- a/tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php +++ b/tests/Security/EnsureSAMLUserForSAMLLoginCheckerTest.php @@ -30,7 +30,7 @@ use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; -class EnsureSAMLUserForSAMLLoginCheckerTest extends WebTestCase +final class EnsureSAMLUserForSAMLLoginCheckerTest extends WebTestCase { /** @var EnsureSAMLUserForSAMLLoginChecker */ protected $service; diff --git a/tests/Security/SamlUserFactoryTest.php b/tests/Security/SamlUserFactoryTest.php index 7780b4be..b975ca0d 100644 --- a/tests/Security/SamlUserFactoryTest.php +++ b/tests/Security/SamlUserFactoryTest.php @@ -26,7 +26,7 @@ use App\Entity\UserSystem\User; use App\Security\SamlUserFactory; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class SamlUserFactoryTest extends WebTestCase +final class SamlUserFactoryTest extends WebTestCase { /** @var SamlUserFactory */ diff --git a/tests/Security/UserCheckerTest.php b/tests/Security/UserCheckerTest.php index 35c2e1e5..e32d5bfe 100644 --- a/tests/Security/UserCheckerTest.php +++ b/tests/Security/UserCheckerTest.php @@ -27,7 +27,7 @@ use App\Security\UserChecker; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; -class UserCheckerTest extends WebTestCase +final class UserCheckerTest extends WebTestCase { protected $service; diff --git a/tests/Serializer/BigNumberNormalizerTest.php b/tests/Serializer/BigNumberNormalizerTest.php index f64347ee..509d6352 100644 --- a/tests/Serializer/BigNumberNormalizerTest.php +++ b/tests/Serializer/BigNumberNormalizerTest.php @@ -29,7 +29,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Brick\Math\BigDecimal; use Brick\Math\BigNumber; -class BigNumberNormalizerTest extends WebTestCase +final class BigNumberNormalizerTest extends WebTestCase { /** @var BigNumberNormalizer */ protected $service; diff --git a/tests/Serializer/PartNormalizerTest.php b/tests/Serializer/PartNormalizerTest.php index 9baff750..2f07f36d 100644 --- a/tests/Serializer/PartNormalizerTest.php +++ b/tests/Serializer/PartNormalizerTest.php @@ -31,7 +31,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -class PartNormalizerTest extends WebTestCase +final class PartNormalizerTest extends WebTestCase { /** @var PartNormalizer */ protected DenormalizerInterface&NormalizerInterface $service; diff --git a/tests/Serializer/StructuralElementDenormalizerTest.php b/tests/Serializer/StructuralElementDenormalizerTest.php index 31c9f0bb..e8e46611 100644 --- a/tests/Serializer/StructuralElementDenormalizerTest.php +++ b/tests/Serializer/StructuralElementDenormalizerTest.php @@ -27,7 +27,7 @@ use App\Entity\Parts\Category; use App\Serializer\StructuralElementDenormalizer; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class StructuralElementDenormalizerTest extends WebTestCase +final class StructuralElementDenormalizerTest extends WebTestCase { /** @var StructuralElementDenormalizer */ diff --git a/tests/Serializer/StructuralElementFromNameDenormalizerTest.php b/tests/Serializer/StructuralElementFromNameDenormalizerTest.php index b344508c..b4bdcdac 100644 --- a/tests/Serializer/StructuralElementFromNameDenormalizerTest.php +++ b/tests/Serializer/StructuralElementFromNameDenormalizerTest.php @@ -26,7 +26,7 @@ use App\Entity\Parts\Category; use App\Serializer\StructuralElementFromNameDenormalizer; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class StructuralElementFromNameDenormalizerTest extends WebTestCase +final class StructuralElementFromNameDenormalizerTest extends WebTestCase { /** @var StructuralElementFromNameDenormalizer */ diff --git a/tests/Serializer/StructuralElementNormalizerTest.php b/tests/Serializer/StructuralElementNormalizerTest.php index 79f739fa..4b335bcd 100644 --- a/tests/Serializer/StructuralElementNormalizerTest.php +++ b/tests/Serializer/StructuralElementNormalizerTest.php @@ -30,7 +30,7 @@ use App\Serializer\StructuralElementNormalizer; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class StructuralElementNormalizerTest extends WebTestCase +final class StructuralElementNormalizerTest extends WebTestCase { /** @var StructuralElementNormalizer */ diff --git a/tests/Services/Attachments/AttachmentPathResolverTest.php b/tests/Services/Attachments/AttachmentPathResolverTest.php index 69658e13..b145e482 100644 --- a/tests/Services/Attachments/AttachmentPathResolverTest.php +++ b/tests/Services/Attachments/AttachmentPathResolverTest.php @@ -28,10 +28,8 @@ use App\Services\Attachments\AttachmentPathResolver; use const DIRECTORY_SEPARATOR; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class AttachmentPathResolverTest extends WebTestCase +final class AttachmentPathResolverTest extends WebTestCase { - protected string $media_path; - protected string $footprint_path; protected $projectDir_orig; protected $projectDir; /** @@ -46,8 +44,8 @@ class AttachmentPathResolverTest extends WebTestCase $this->projectDir_orig = realpath(self::$kernel->getProjectDir()); $this->projectDir = str_replace('\\', '/', $this->projectDir_orig); - $this->media_path = $this->projectDir.'/public/media'; - $this->footprint_path = $this->projectDir.'/public/img/footprints'; + $media_path = $this->projectDir.'/public/media'; + $footprint_path = $this->projectDir.'/public/img/footprints'; $this->service = self::getContainer()->get(AttachmentPathResolver::class); } diff --git a/tests/Services/Attachments/AttachmentURLGeneratorTest.php b/tests/Services/Attachments/AttachmentURLGeneratorTest.php index e9e6d992..4359c1b9 100644 --- a/tests/Services/Attachments/AttachmentURLGeneratorTest.php +++ b/tests/Services/Attachments/AttachmentURLGeneratorTest.php @@ -26,7 +26,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\Services\Attachments\AttachmentURLGenerator; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class AttachmentURLGeneratorTest extends WebTestCase +final class AttachmentURLGeneratorTest extends WebTestCase { protected const PUBLIC_DIR = '/public'; diff --git a/tests/Services/Attachments/BuiltinAttachmentsFinderTest.php b/tests/Services/Attachments/BuiltinAttachmentsFinderTest.php index 80c699ac..5198ddea 100644 --- a/tests/Services/Attachments/BuiltinAttachmentsFinderTest.php +++ b/tests/Services/Attachments/BuiltinAttachmentsFinderTest.php @@ -26,7 +26,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\Services\Attachments\BuiltinAttachmentsFinder; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class BuiltinAttachmentsFinderTest extends WebTestCase +final class BuiltinAttachmentsFinderTest extends WebTestCase { protected static array $mock_list = [ '%FOOTPRINTS%/test/test.jpg', '%FOOTPRINTS%/test/test.png', '%FOOTPRINTS%/123.jpg', '%FOOTPRINTS%/123.jpeg', diff --git a/tests/Services/Attachments/FileTypeFilterToolsTest.php b/tests/Services/Attachments/FileTypeFilterToolsTest.php index 1b85eaeb..f089feec 100644 --- a/tests/Services/Attachments/FileTypeFilterToolsTest.php +++ b/tests/Services/Attachments/FileTypeFilterToolsTest.php @@ -26,7 +26,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\Services\Attachments\FileTypeFilterTools; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class FileTypeFilterToolsTest extends WebTestCase +final class FileTypeFilterToolsTest extends WebTestCase { protected static $service; diff --git a/tests/Services/ElementTypeNameGeneratorTest.php b/tests/Services/ElementTypeNameGeneratorTest.php index 8739dd06..21797137 100644 --- a/tests/Services/ElementTypeNameGeneratorTest.php +++ b/tests/Services/ElementTypeNameGeneratorTest.php @@ -35,7 +35,7 @@ use App\Services\Formatters\AmountFormatter; use App\Settings\SynonymSettings; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class ElementTypeNameGeneratorTest extends WebTestCase +final class ElementTypeNameGeneratorTest extends WebTestCase { protected ElementTypeNameGenerator $service; private SynonymSettings $synonymSettings; diff --git a/tests/Services/ElementTypesTest.php b/tests/Services/ElementTypesTest.php index d4fa77ff..15e5d8bb 100644 --- a/tests/Services/ElementTypesTest.php +++ b/tests/Services/ElementTypesTest.php @@ -1,4 +1,7 @@ . */ - namespace App\Tests\Services; use App\Entity\Parameters\CategoryParameter; @@ -26,7 +28,7 @@ use App\Exceptions\EntityNotSupportedException; use App\Services\ElementTypes; use PHPUnit\Framework\TestCase; -class ElementTypesTest extends TestCase +final class ElementTypesTest extends TestCase { public function testFromClass(): void diff --git a/tests/Services/EntityMergers/Mergers/EntityMergerHelperTraitTest.php b/tests/Services/EntityMergers/Mergers/EntityMergerHelperTraitTest.php index 22fa220b..f5fd8334 100644 --- a/tests/Services/EntityMergers/Mergers/EntityMergerHelperTraitTest.php +++ b/tests/Services/EntityMergers/Mergers/EntityMergerHelperTraitTest.php @@ -28,7 +28,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -class EntityMergerHelperTraitTest extends KernelTestCase +final class EntityMergerHelperTraitTest extends KernelTestCase { use EntityMergerHelperTrait; diff --git a/tests/Services/EntityMergers/Mergers/PartMergerTest.php b/tests/Services/EntityMergers/Mergers/PartMergerTest.php index 7db4ddd6..f6a75790 100644 --- a/tests/Services/EntityMergers/Mergers/PartMergerTest.php +++ b/tests/Services/EntityMergers/Mergers/PartMergerTest.php @@ -36,7 +36,7 @@ use App\Services\EntityMergers\Mergers\PartMerger; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -class PartMergerTest extends KernelTestCase +final class PartMergerTest extends KernelTestCase { /** @var PartMerger|null */ diff --git a/tests/Services/Formatters/AmountFormatterTest.php b/tests/Services/Formatters/AmountFormatterTest.php index 40f9b7cf..9fdeb441 100644 --- a/tests/Services/Formatters/AmountFormatterTest.php +++ b/tests/Services/Formatters/AmountFormatterTest.php @@ -27,7 +27,7 @@ use App\Services\Formatters\AmountFormatter; use InvalidArgumentException; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class AmountFormatterTest extends WebTestCase +final class AmountFormatterTest extends WebTestCase { /** * @var AmountFormatter diff --git a/tests/Services/Formatters/SIFormatterTest.php b/tests/Services/Formatters/SIFormatterTest.php index 79668589..62ca0187 100644 --- a/tests/Services/Formatters/SIFormatterTest.php +++ b/tests/Services/Formatters/SIFormatterTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Services\Formatters; use App\Services\Formatters\SIFormatter; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class SIFormatterTest extends WebTestCase +final class SIFormatterTest extends WebTestCase { /** * @var SIFormatter diff --git a/tests/Services/ImportExportSystem/BOMImporterTest.php b/tests/Services/ImportExportSystem/BOMImporterTest.php index a8841f17..5a9d4121 100644 --- a/tests/Services/ImportExportSystem/BOMImporterTest.php +++ b/tests/Services/ImportExportSystem/BOMImporterTest.php @@ -22,6 +22,8 @@ declare(strict_types=1); */ namespace App\Tests\Services\ImportExportSystem; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\Parts\Category; use App\Entity\Parts\Part; use App\Entity\Parts\Supplier; use App\Entity\ProjectSystem\Project; @@ -31,7 +33,7 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\File\File; -class BOMImporterTest extends WebTestCase +final class BOMImporterTest extends WebTestCase { /** @@ -391,7 +393,7 @@ class BOMImporterTest extends WebTestCase // Check first entry $this->assertEquals('R1,R2', $bom_entries[0]->getMountnames()); - $this->assertEquals(2.0, $bom_entries[0]->getQuantity()); + $this->assertEqualsWithDelta(2.0, $bom_entries[0]->getQuantity(), PHP_FLOAT_EPSILON); $this->assertEquals('CRCW080510K0FKEA (R_0805_2012Metric)', $bom_entries[0]->getName()); $this->assertStringContainsString('Value: 10k', $bom_entries[0]->getComment()); $this->assertStringContainsString('MPN: CRCW080510K0FKEA', $bom_entries[0]->getComment()); @@ -402,7 +404,7 @@ class BOMImporterTest extends WebTestCase // Check second entry $this->assertEquals('C1', $bom_entries[1]->getMountnames()); - $this->assertEquals(1.0, $bom_entries[1]->getQuantity()); + $this->assertEqualsWithDelta(1.0, $bom_entries[1]->getQuantity(), PHP_FLOAT_EPSILON); $this->assertStringContainsString('LCSC SPN: C789012', $bom_entries[1]->getComment()); $this->assertStringContainsString('Mouser SPN: 80-CL21A104KOCLRNC', $bom_entries[1]->getComment()); @@ -542,7 +544,7 @@ class BOMImporterTest extends WebTestCase $this->assertCount(1, $bom_entries); // Should merge into one entry $this->assertEquals('R1,R2', $bom_entries[0]->getMountnames()); - $this->assertEquals(2.0, $bom_entries[0]->getQuantity()); + $this->assertEqualsWithDelta(2.0, $bom_entries[0]->getQuantity(), PHP_FLOAT_EPSILON); $this->assertEquals('CRCW080510K0FKEA', $bom_entries[0]->getName()); } @@ -630,7 +632,7 @@ class BOMImporterTest extends WebTestCase $this->entityManager->persist($part); // Create orderdetail linking the part to a supplier SPN - $orderdetail = new \App\Entity\PriceInformations\Orderdetail(); + $orderdetail = new Orderdetail(); $orderdetail->setPart($part); $orderdetail->setSupplier($lcscSupplier); $orderdetail->setSupplierpartnr('C123456'); @@ -664,7 +666,7 @@ class BOMImporterTest extends WebTestCase $this->assertSame($part, $bom_entries[0]->getPart()); $this->assertEquals('Test Resistor 10k 0805', $bom_entries[0]->getName()); $this->assertEquals('R1,R2', $bom_entries[0]->getMountnames()); - $this->assertEquals(2.0, $bom_entries[0]->getQuantity()); + $this->assertEqualsWithDelta(2.0, $bom_entries[0]->getQuantity(), PHP_FLOAT_EPSILON); $this->assertStringContainsString('LCSC SPN: C123456', $bom_entries[0]->getComment()); $this->assertStringContainsString('Part-DB ID: ' . $part->getID(), $bom_entries[0]->getComment()); @@ -691,7 +693,7 @@ class BOMImporterTest extends WebTestCase $part1->setCategory($this->getDefaultCategory($this->entityManager)); $this->entityManager->persist($part1); - $orderdetail1 = new \App\Entity\PriceInformations\Orderdetail(); + $orderdetail1 = new Orderdetail(); $orderdetail1->setPart($part1); $orderdetail1->setSupplier($lcscSupplier); $orderdetail1->setSupplierpartnr('C123456'); @@ -703,7 +705,7 @@ class BOMImporterTest extends WebTestCase $part2->setCategory($this->getDefaultCategory($this->entityManager)); $this->entityManager->persist($part2); - $orderdetail2 = new \App\Entity\PriceInformations\Orderdetail(); + $orderdetail2 = new Orderdetail(); $orderdetail2->setPart($part2); $orderdetail2->setSupplier($mouserSupplier); $orderdetail2->setSupplierpartnr('789-CAP100NF'); @@ -794,12 +796,12 @@ class BOMImporterTest extends WebTestCase private function getDefaultCategory(EntityManagerInterface $entityManager) { // Get the first available category or create a default one - $categoryRepo = $entityManager->getRepository(\App\Entity\Parts\Category::class); + $categoryRepo = $entityManager->getRepository(Category::class); $categories = $categoryRepo->findAll(); if (empty($categories)) { // Create a default category if none exists - $category = new \App\Entity\Parts\Category(); + $category = new Category(); $category->setName('Default Category'); $entityManager->persist($category); $entityManager->flush(); diff --git a/tests/Services/ImportExportSystem/BOMValidationServiceTest.php b/tests/Services/ImportExportSystem/BOMValidationServiceTest.php index 055db8b4..a6c103db 100644 --- a/tests/Services/ImportExportSystem/BOMValidationServiceTest.php +++ b/tests/Services/ImportExportSystem/BOMValidationServiceTest.php @@ -32,18 +32,16 @@ use Symfony\Contracts\Translation\TranslatorInterface; /** * @see \App\Services\ImportExportSystem\BOMValidationService */ -class BOMValidationServiceTest extends WebTestCase +final class BOMValidationServiceTest extends WebTestCase { private BOMValidationService $validationService; - private EntityManagerInterface $entityManager; - private TranslatorInterface $translator; protected function setUp(): void { self::bootKernel(); - $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); - $this->translator = self::getContainer()->get(TranslatorInterface::class); - $this->validationService = new BOMValidationService($this->entityManager, $this->translator); + $entityManager = self::getContainer()->get(EntityManagerInterface::class); + $translator = self::getContainer()->get(TranslatorInterface::class); + $this->validationService = new BOMValidationService($entityManager, $translator); } public function testValidateBOMEntryWithValidData(): void @@ -244,7 +242,7 @@ class BOMValidationServiceTest extends WebTestCase $this->assertTrue($result['is_valid']); $this->assertCount(1, $result['info']); - $this->assertStringContainsString('library prefix', $result['info'][0]); + $this->assertStringContainsString('library prefix', (string) $result['info'][0]); } public function testValidateBOMEntriesWithMultipleEntries(): void @@ -314,7 +312,7 @@ class BOMValidationServiceTest extends WebTestCase $this->assertEquals(2, $stats['error_count']); $this->assertEquals(1, $stats['warning_count']); $this->assertEquals(2, $stats['info_count']); - $this->assertEquals(80.0, $stats['success_rate']); + $this->assertEqualsWithDelta(80.0, $stats['success_rate'], PHP_FLOAT_EPSILON); } public function testGetErrorMessage(): void @@ -344,6 +342,6 @@ class BOMValidationServiceTest extends WebTestCase $message = $this->validationService->getErrorMessage($validation_result); - $this->assertEquals('', $message); + $this->assertSame('', $message); } } \ No newline at end of file diff --git a/tests/Services/ImportExportSystem/EntityExporterTest.php b/tests/Services/ImportExportSystem/EntityExporterTest.php index e9b924b1..e4518961 100644 --- a/tests/Services/ImportExportSystem/EntityExporterTest.php +++ b/tests/Services/ImportExportSystem/EntityExporterTest.php @@ -28,7 +28,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Request; use PhpOffice\PhpSpreadsheet\IOFactory; -class EntityExporterTest extends WebTestCase +final class EntityExporterTest extends WebTestCase { /** * @var EntityExporter @@ -43,9 +43,9 @@ class EntityExporterTest extends WebTestCase private function getEntities(): array { - $entity1 = (new Category())->setName('Enitity 1')->setComment('Test'); - $entity1_1 = (new Category())->setName('Enitity 1.1')->setParent($entity1); - $entity2 = (new Category())->setName('Enitity 2'); + $entity1 = (new Category())->setName('Entity%1')->setComment('Test'); + $entity1_1 = (new Category())->setName('Entity 1.1')->setParent($entity1); + $entity2 = (new Category())->setName('Entity 2'); return [$entity1, $entity1_1, $entity2]; } @@ -55,12 +55,12 @@ class EntityExporterTest extends WebTestCase $entities = $this->getEntities(); $json_without_children = $this->service->exportEntities($entities, ['format' => 'json', 'level' => 'simple']); - $this->assertJsonStringEqualsJsonString('[{"name":"Enitity 1","type":"category","full_name":"Enitity 1"},{"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"},{"name":"Enitity 2","type":"category","full_name":"Enitity 2"}]', + $this->assertJsonStringEqualsJsonString('[{"name":"Entity%1","type":"category","full_name":"Entity%1"},{"name":"Entity 1.1","type":"category","full_name":"Entity%1->Entity 1.1"},{"name":"Entity 2","type":"category","full_name":"Entity 2"}]', $json_without_children); $json_with_children = $this->service->exportEntities($entities, ['format' => 'json', 'level' => 'simple', 'include_children' => true]); - $this->assertJsonStringEqualsJsonString('[{"children":[{"children":[],"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"}],"name":"Enitity 1","type":"category","full_name":"Enitity 1"},{"children":[],"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"},{"children":[],"name":"Enitity 2","type":"category","full_name":"Enitity 2"}]', + $this->assertJsonStringEqualsJsonString('[{"children":[{"children":[],"name":"Entity 1.1","type":"category","full_name":"Entity%1->Entity 1.1"}],"name":"Entity%1","type":"category","full_name":"Entity%1"},{"children":[],"name":"Entity 1.1","type":"category","full_name":"Entity%1->Entity 1.1"},{"children":[],"name":"Entity 2","type":"category","full_name":"Entity 2"}]', $json_with_children); } @@ -95,8 +95,8 @@ class EntityExporterTest extends WebTestCase $this->assertSame('name', $worksheet->getCell('A1')->getValue()); $this->assertSame('full_name', $worksheet->getCell('B1')->getValue()); - $this->assertSame('Enitity 1', $worksheet->getCell('A2')->getValue()); - $this->assertSame('Enitity 1', $worksheet->getCell('B2')->getValue()); + $this->assertSame('Entity%1', $worksheet->getCell('A2')->getValue()); + $this->assertSame('Entity%1', $worksheet->getCell('B2')->getValue()); unlink($tempFile); } @@ -111,6 +111,6 @@ class EntityExporterTest extends WebTestCase $response = $this->service->exportEntityFromRequest($entities, $request); $this->assertSame('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type')); - $this->assertStringContainsString('export_Category_simple.xlsx', $response->headers->get('Content-Disposition')); + $this->assertStringContainsString('export_Category_simple.xlsx', (string) $response->headers->get('Content-Disposition')); } } diff --git a/tests/Services/ImportExportSystem/EntityImporterTest.php b/tests/Services/ImportExportSystem/EntityImporterTest.php index 83367f80..b0044dda 100644 --- a/tests/Services/ImportExportSystem/EntityImporterTest.php +++ b/tests/Services/ImportExportSystem/EntityImporterTest.php @@ -41,7 +41,7 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; #[Group('DB')] -class EntityImporterTest extends WebTestCase +final class EntityImporterTest extends WebTestCase { /** * @var EntityImporter diff --git a/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php index e300e2bf..2fd50f9a 100644 --- a/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php +++ b/tests/Services/InfoProviderSystem/DTOs/BulkSearchFieldMappingDTOTest.php @@ -1,4 +1,7 @@ . */ - namespace App\Tests\Services\InfoProviderSystem\DTOs; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Services\InfoProviderSystem\DTOs\BulkSearchFieldMappingDTO; use PHPUnit\Framework\TestCase; -class BulkSearchFieldMappingDTOTest extends TestCase +final class BulkSearchFieldMappingDTOTest extends TestCase { public function testProviderInstanceNormalization(): void { - $mockProvider = $this->createMock(\App\Services\InfoProviderSystem\Providers\InfoProviderInterface::class); + $mockProvider = $this->createMock(InfoProviderInterface::class); $mockProvider->method('getProviderKey')->willReturn('mock_provider'); $fieldMapping = new BulkSearchFieldMappingDTO(field: 'mpn', providers: ['provider1', $mockProvider], priority: 5); diff --git a/tests/Services/InfoProviderSystem/DTOs/BulkSearchPartResultsDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/BulkSearchPartResultsDTOTest.php index 09fa4973..d3170d9e 100644 --- a/tests/Services/InfoProviderSystem/DTOs/BulkSearchPartResultsDTOTest.php +++ b/tests/Services/InfoProviderSystem/DTOs/BulkSearchPartResultsDTOTest.php @@ -1,4 +1,7 @@ . */ - namespace App\Tests\Services\InfoProviderSystem\DTOs; +use App\Entity\Parts\Part; +use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO; use App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultsDTO; use PHPUnit\Framework\TestCase; -class BulkSearchPartResultsDTOTest extends TestCase +final class BulkSearchPartResultsDTOTest extends TestCase { public function testHasErrors(): void { - $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], []); + $test = new BulkSearchPartResultsDTO($this->createStub(Part::class), [], []); $this->assertFalse($test->hasErrors()); - $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], ['error1']); + $test = new BulkSearchPartResultsDTO($this->createStub(Part::class), [], ['error1']); $this->assertTrue($test->hasErrors()); } public function testGetErrorCount(): void { - $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], []); + $test = new BulkSearchPartResultsDTO($this->createStub(Part::class), [], []); $this->assertCount(0, $test->errors); - $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], ['error1', 'error2']); + $test = new BulkSearchPartResultsDTO($this->createStub(Part::class), [], ['error1', 'error2']); $this->assertCount(2, $test->errors); } public function testHasResults(): void { - $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], []); + $test = new BulkSearchPartResultsDTO($this->createStub(Part::class), [], []); $this->assertFalse($test->hasResults()); - $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [ $this->createMock(\App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO::class) ], []); + $test = new BulkSearchPartResultsDTO($this->createStub(Part::class), [ $this->createStub(BulkSearchPartResultDTO::class) ], []); $this->assertTrue($test->hasResults()); } public function testGetResultCount(): void { - $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [], []); + $test = new BulkSearchPartResultsDTO($this->createStub(Part::class), [], []); $this->assertCount(0, $test->searchResults); - $test = new BulkSearchPartResultsDTO($this->createMock(\App\Entity\Parts\Part::class), [ - $this->createMock(\App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO::class), - $this->createMock(\App\Services\InfoProviderSystem\DTOs\BulkSearchPartResultDTO::class) + $test = new BulkSearchPartResultsDTO($this->createStub(Part::class), [ + $this->createStub(BulkSearchPartResultDTO::class), + $this->createStub(BulkSearchPartResultDTO::class) ], []); $this->assertCount(2, $test->searchResults); } diff --git a/tests/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTOTest.php index b4dc0dea..f79d8ce8 100644 --- a/tests/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTOTest.php +++ b/tests/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTOTest.php @@ -1,4 +1,7 @@ . */ - namespace App\Tests\Services\InfoProviderSystem\DTOs; use App\Entity\Parts\Part; @@ -29,7 +31,7 @@ use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -class BulkSearchResponseDTOTest extends KernelTestCase +final class BulkSearchResponseDTOTest extends KernelTestCase { private EntityManagerInterface $entityManager; @@ -108,6 +110,7 @@ class BulkSearchResponseDTOTest extends KernelTestCase 'manufacturing_status' => NULL, 'provider_url' => NULL, 'footprint' => NULL, + 'gtin' => NULL, ), 'source_field' => 'mpn', 'source_keyword' => '1234', @@ -129,6 +132,7 @@ class BulkSearchResponseDTOTest extends KernelTestCase 'manufacturing_status' => NULL, 'provider_url' => NULL, 'footprint' => NULL, + 'gtin' => NULL, ), 'source_field' => 'name', 'source_keyword' => '1234', diff --git a/tests/Services/InfoProviderSystem/DTOs/FileDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/FileDTOTest.php index 10312aca..fe563fb1 100644 --- a/tests/Services/InfoProviderSystem/DTOs/FileDTOTest.php +++ b/tests/Services/InfoProviderSystem/DTOs/FileDTOTest.php @@ -26,7 +26,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\Services\InfoProviderSystem\DTOs\FileDTO; use PHPUnit\Framework\TestCase; -class FileDTOTest extends TestCase +final class FileDTOTest extends TestCase { diff --git a/tests/Services/InfoProviderSystem/DTOs/ParameterDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/ParameterDTOTest.php index 4c4e9bfe..6361dc10 100644 --- a/tests/Services/InfoProviderSystem/DTOs/ParameterDTOTest.php +++ b/tests/Services/InfoProviderSystem/DTOs/ParameterDTOTest.php @@ -26,7 +26,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use PHPUnit\Framework\TestCase; -class ParameterDTOTest extends TestCase +final class ParameterDTOTest extends TestCase { public static function parseValueFieldDataProvider(): \Generator diff --git a/tests/Services/InfoProviderSystem/DTOs/PurchaseInfoDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/PurchaseInfoDTOTest.php index 14a3c03f..480ff924 100644 --- a/tests/Services/InfoProviderSystem/DTOs/PurchaseInfoDTOTest.php +++ b/tests/Services/InfoProviderSystem/DTOs/PurchaseInfoDTOTest.php @@ -22,10 +22,11 @@ declare(strict_types=1); */ namespace App\Tests\Services\InfoProviderSystem\DTOs; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; use PHPUnit\Framework\TestCase; -class PurchaseInfoDTOTest extends TestCase +final class PurchaseInfoDTOTest extends TestCase { public function testThrowOnInvalidType(): void { @@ -33,4 +34,40 @@ class PurchaseInfoDTOTest extends TestCase $this->expectExceptionMessage('The prices array must only contain PriceDTO instances'); new PurchaseInfoDTO('test', 'test', [new \stdClass()]); } + + public function testPricesIncludesVATHandling(): void + { + $pricesTrue = [ + new PriceDTO(minimum_discount_amount: 1, price: '10.00', currency_iso_code: 'USD', includes_tax: true), + new PriceDTO(minimum_discount_amount: 5, price: '9.00', currency_iso_code: 'USD', includes_tax: true), + ]; + $pricesFalse = [ + new PriceDTO(minimum_discount_amount: 1, price: '10.00', currency_iso_code: 'USD', includes_tax: false), + new PriceDTO(minimum_discount_amount: 5, price: '9.00', currency_iso_code: 'USD', includes_tax: false), + ]; + $pricesMixed = [ + new PriceDTO(minimum_discount_amount: 1, price: '10.00', currency_iso_code: 'USD', includes_tax: true), + new PriceDTO(minimum_discount_amount: 5, price: '9.00', currency_iso_code: 'USD', includes_tax: false), + ]; + $pricesNull = [ + new PriceDTO(minimum_discount_amount: 1, price: '10.00', currency_iso_code: 'USD', includes_tax: null), + new PriceDTO(minimum_discount_amount: 5, price: '9.00', currency_iso_code: 'USD', includes_tax: null), + ]; + + //If the prices_include_vat parameter is given, use it: + $dto = new PurchaseInfoDTO('test', 'test', $pricesMixed, prices_include_vat: true); + $this->assertTrue($dto->prices_include_vat); + $dto = new PurchaseInfoDTO('test', 'test', $pricesMixed, prices_include_vat: false); + $this->assertFalse($dto->prices_include_vat); + + //If the prices_include_vat parameter is not given, try to deduct it from the prices: + $dto = new PurchaseInfoDTO('test', 'test', $pricesTrue); + $this->assertTrue($dto->prices_include_vat); + $dto = new PurchaseInfoDTO('test', 'test', $pricesFalse); + $this->assertFalse($dto->prices_include_vat); + $dto = new PurchaseInfoDTO('test', 'test', $pricesMixed); + $this->assertNull($dto->prices_include_vat); + $dto = new PurchaseInfoDTO('test', 'test', $pricesNull); + $this->assertNull($dto->prices_include_vat); + } } diff --git a/tests/Services/InfoProviderSystem/DTOs/SearchResultDTOTest.php b/tests/Services/InfoProviderSystem/DTOs/SearchResultDTOTest.php index dd516c8d..4fbac1f3 100644 --- a/tests/Services/InfoProviderSystem/DTOs/SearchResultDTOTest.php +++ b/tests/Services/InfoProviderSystem/DTOs/SearchResultDTOTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Services\InfoProviderSystem\DTOs; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use PHPUnit\Framework\TestCase; -class SearchResultDTOTest extends TestCase +final class SearchResultDTOTest extends TestCase { public function testPreviewImageURL(): void { diff --git a/tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php b/tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php index 6c933472..8ea6c71a 100644 --- a/tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php +++ b/tests/Services/InfoProviderSystem/DTOtoEntityConverterTest.php @@ -35,7 +35,7 @@ use PhpParser\Node\Param; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class DTOtoEntityConverterTest extends WebTestCase +final class DTOtoEntityConverterTest extends WebTestCase { private ?DTOtoEntityConverter $service = null; @@ -94,7 +94,6 @@ class DTOtoEntityConverterTest extends WebTestCase minimum_discount_amount: 5, price: "10.0", currency_iso_code: 'EUR', - includes_tax: true, ); $entity = $this->service->convertPrice($dto); @@ -115,6 +114,7 @@ class DTOtoEntityConverterTest extends WebTestCase order_number: 'TestOrderNumber', prices: $prices, product_url: 'https://example.com', + prices_include_vat: true, ); $entity = $this->service->convertPurchaseInfo($dto); @@ -122,6 +122,7 @@ class DTOtoEntityConverterTest extends WebTestCase $this->assertSame($dto->distributor_name, $entity->getSupplier()->getName()); $this->assertSame($dto->order_number, $entity->getSupplierPartNr()); $this->assertEquals($dto->product_url, $entity->getSupplierProductUrl()); + $this->assertTrue($dto->prices_include_vat); } public function testConvertFileWithName(): void @@ -159,12 +160,13 @@ class DTOtoEntityConverterTest extends WebTestCase $shopping_infos = [new PurchaseInfoDTO('TestDistributor', 'TestOrderNumber', [new PriceDTO(1, "10.0", 'EUR')])]; $dto = new PartDetailDTO( - provider_key: 'test_provider', provider_id: 'test_id', provider_url: 'https://invalid.invalid/test_id', - name: 'TestPart', description: 'TestDescription', category: 'TestCategory', - manufacturer: 'TestManufacturer', mpn: 'TestMPN', manufacturing_status: ManufacturingStatus::EOL, - preview_image_url: 'https://invalid.invalid/image.png', - footprint: 'DIP8', notes: 'TestNotes', mass: 10.4, - parameters: $parameters, datasheets: $datasheets, vendor_infos: $shopping_infos, images: $images + provider_key: 'test_provider', provider_id: 'test_id', name: 'TestPart', + description: 'TestDescription', category: 'TestCategory', manufacturer: 'TestManufacturer', + mpn: 'TestMPN', preview_image_url: 'https://invalid.invalid/image.png', + manufacturing_status: ManufacturingStatus::EOL, + provider_url: 'https://invalid.invalid/test_id', + footprint: 'DIP8', gtin: "1234567890123", notes: 'TestNotes', datasheets: $datasheets, + images: $images, parameters: $parameters, vendor_infos: $shopping_infos, mass: 10.4 ); $entity = $this->service->convertPart($dto); @@ -180,6 +182,8 @@ class DTOtoEntityConverterTest extends WebTestCase $this->assertEquals($dto->mass, $entity->getMass()); $this->assertEquals($dto->footprint, $entity->getFootprint()); + $this->assertEquals($dto->gtin, $entity->getGtin()); + //We just check that the lenghts of parameters, datasheets, images and shopping infos are the same //The actual content is tested in the corresponding tests $this->assertCount(count($parameters), $entity->getParameters()); diff --git a/tests/Services/InfoProviderSystem/ProviderRegistryTest.php b/tests/Services/InfoProviderSystem/ProviderRegistryTest.php index 48a1847f..d3fce441 100644 --- a/tests/Services/InfoProviderSystem/ProviderRegistryTest.php +++ b/tests/Services/InfoProviderSystem/ProviderRegistryTest.php @@ -27,7 +27,7 @@ use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Services\InfoProviderSystem\Providers\URLHandlerInfoProviderInterface; use PHPUnit\Framework\TestCase; -class ProviderRegistryTest extends TestCase +final class ProviderRegistryTest extends TestCase { /** @var InfoProviderInterface[] */ diff --git a/tests/Services/InfoProviderSystem/Providers/BuerklinProviderTest.php b/tests/Services/InfoProviderSystem/Providers/BuerklinProviderTest.php index 8283b7d3..ef446c9a 100644 --- a/tests/Services/InfoProviderSystem/Providers/BuerklinProviderTest.php +++ b/tests/Services/InfoProviderSystem/Providers/BuerklinProviderTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Tests\Services\InfoProviderSystem\Providers; +use PHPUnit\Framework\Attributes\DataProvider; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; use App\Services\InfoProviderSystem\Providers\BuerklinProvider; @@ -18,7 +19,7 @@ use Symfony\Contracts\HttpClient\ResponseInterface; * Full behavioral test suite for BuerklinProvider. * Includes parameter parsing, compliance parsing, images, prices and batch mode. */ -class BuerklinProviderTest extends TestCase +final class BuerklinProviderTest extends TestCase { private HttpClientInterface $httpClient; private CacheItemPoolInterface $cache; @@ -108,14 +109,14 @@ class BuerklinProviderTest extends TestCase $this->assertSame('Zener voltage', $params[0]->name); $this->assertNull($params[0]->value_text); - $this->assertSame(12.0, $params[0]->value_typ); + $this->assertEqualsWithDelta(12.0, $params[0]->value_typ, PHP_FLOAT_EPSILON); $this->assertNull($params[0]->value_min); $this->assertNull($params[0]->value_max); $this->assertSame('V', $params[0]->unit); $this->assertSame('Length', $params[1]->name); $this->assertNull($params[1]->value_text); - $this->assertSame(2.9, $params[1]->value_typ); + $this->assertEqualsWithDelta(2.9, $params[1]->value_typ, PHP_FLOAT_EPSILON); $this->assertSame('mm', $params[1]->unit); $this->assertSame('Assembly', $params[2]->name); @@ -273,75 +274,70 @@ class BuerklinProviderTest extends TestCase $this->assertSame(['buerklin.com'], $this->provider->getHandledDomains()); } - /** - * @dataProvider buerklinIdFromUrlProvider - */ + #[DataProvider('buerklinIdFromUrlProvider')] public function testGetIDFromURLExtractsId(string $url, ?string $expected): void { $this->assertSame($expected, $this->provider->getIDFromURL($url)); } - public static function buerklinIdFromUrlProvider(): array + public static function buerklinIdFromUrlProvider(): \Iterator { - return [ - 'de long path' => [ - 'https://www.buerklin.com/de/p/bkl-electronic/niedervoltsteckverbinder/072341-l/40F1332/', - '40F1332', - ], - 'de short path' => [ - 'https://www.buerklin.com/de/p/40F1332/', - '40F1332', - ], - 'en long path' => [ - 'https://www.buerklin.com/en/p/bkl-electronic/dc-connectors/072341-l/40F1332/', - '40F1332', - ], - 'en short path' => [ - 'https://www.buerklin.com/en/p/40F1332/', - '40F1332', - ], - 'fragment should be ignored' => [ - 'https://www.buerklin.com/de/p/bkl-electronic/niedervoltsteckverbinder/072341-l/40F1332/#download', - '40F1332', - ], - 'no trailing slash' => [ - 'https://www.buerklin.com/en/p/40F1332', - '40F1332', - ], - 'query should be ignored' => [ - 'https://www.buerklin.com/en/p/40F1332/?foo=bar', - '40F1332', - ], - 'query and fragment should be ignored' => [ - 'https://www.buerklin.com/en/p/40F1332/?foo=bar#download', - '40F1332', - ], - - // Negative cases - 'not a product url (no /p/ segment)' => [ - 'https://www.buerklin.com/de/impressum/', - null, - ], - 'path contains "p" but not "/p/"' => [ - 'https://www.buerklin.com/de/help/price/', - null, - ], - 'ends with /p/ (no id)' => [ - 'https://www.buerklin.com/de/p/', - null, - ], - 'ends with /p (no trailing slash)' => [ - 'https://www.buerklin.com/de/p', - null, - ], - 'empty string' => [ - '', - null, - ], - 'not a url string' => [ - 'not a url', - null, - ], + yield 'de long path' => [ + 'https://www.buerklin.com/de/p/bkl-electronic/niedervoltsteckverbinder/072341-l/40F1332/', + '40F1332', + ]; + yield 'de short path' => [ + 'https://www.buerklin.com/de/p/40F1332/', + '40F1332', + ]; + yield 'en long path' => [ + 'https://www.buerklin.com/en/p/bkl-electronic/dc-connectors/072341-l/40F1332/', + '40F1332', + ]; + yield 'en short path' => [ + 'https://www.buerklin.com/en/p/40F1332/', + '40F1332', + ]; + yield 'fragment should be ignored' => [ + 'https://www.buerklin.com/de/p/bkl-electronic/niedervoltsteckverbinder/072341-l/40F1332/#download', + '40F1332', + ]; + yield 'no trailing slash' => [ + 'https://www.buerklin.com/en/p/40F1332', + '40F1332', + ]; + yield 'query should be ignored' => [ + 'https://www.buerklin.com/en/p/40F1332/?foo=bar', + '40F1332', + ]; + yield 'query and fragment should be ignored' => [ + 'https://www.buerklin.com/en/p/40F1332/?foo=bar#download', + '40F1332', + ]; + // Negative cases + yield 'not a product url (no /p/ segment)' => [ + 'https://www.buerklin.com/de/impressum/', + null, + ]; + yield 'path contains "p" but not "/p/"' => [ + 'https://www.buerklin.com/de/help/price/', + null, + ]; + yield 'ends with /p/ (no id)' => [ + 'https://www.buerklin.com/de/p/', + null, + ]; + yield 'ends with /p (no trailing slash)' => [ + 'https://www.buerklin.com/de/p', + null, + ]; + yield 'empty string' => [ + '', + null, + ]; + yield 'not a url string' => [ + 'not a url', + null, ]; } } diff --git a/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php b/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php index 57527f57..dc19de6b 100644 --- a/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php +++ b/tests/Services/InfoProviderSystem/Providers/LCSCProviderTest.php @@ -37,7 +37,7 @@ use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\HttpClientInterface; -class LCSCProviderTest extends TestCase +final class LCSCProviderTest extends TestCase { private LCSCSettings $settings; private LCSCProvider $provider; @@ -67,7 +67,7 @@ class LCSCProviderTest extends TestCase public function testGetProviderKey(): void { - $this->assertEquals('lcsc', $this->provider->getProviderKey()); + $this->assertSame('lcsc', $this->provider->getProviderKey()); } public function testIsActiveWhenEnabled(): void @@ -125,8 +125,8 @@ class LCSCProviderTest extends TestCase $this->assertIsArray($results); $this->assertCount(1, $results); $this->assertInstanceOf(PartDetailDTO::class, $results[0]); - $this->assertEquals('C123456', $results[0]->provider_id); - $this->assertEquals('Test Component', $results[0]->name); + $this->assertSame('C123456', $results[0]->provider_id); + $this->assertSame('Test Component', $results[0]->name); } public function testSearchByKeywordWithRegularTerm(): void @@ -162,8 +162,8 @@ class LCSCProviderTest extends TestCase $this->assertIsArray($results); $this->assertCount(1, $results); $this->assertInstanceOf(PartDetailDTO::class, $results[0]); - $this->assertEquals('C789012', $results[0]->provider_id); - $this->assertEquals('Regular Component', $results[0]->name); + $this->assertSame('C789012', $results[0]->provider_id); + $this->assertSame('Regular Component', $results[0]->name); } public function testSearchByKeywordWithTipProduct(): void @@ -202,8 +202,8 @@ class LCSCProviderTest extends TestCase $this->assertIsArray($results); $this->assertCount(1, $results); $this->assertInstanceOf(PartDetailDTO::class, $results[0]); - $this->assertEquals('C555555', $results[0]->provider_id); - $this->assertEquals('Tip Component', $results[0]->name); + $this->assertSame('C555555', $results[0]->provider_id); + $this->assertSame('Tip Component', $results[0]->name); } public function testSearchByKeywordsBatch(): void @@ -288,12 +288,12 @@ class LCSCProviderTest extends TestCase $result = $this->provider->getDetails('C123456'); $this->assertInstanceOf(PartDetailDTO::class, $result); - $this->assertEquals('C123456', $result->provider_id); - $this->assertEquals('Detailed Component', $result->name); - $this->assertEquals('Detailed description', $result->description); - $this->assertEquals('Detailed Manufacturer', $result->manufacturer); + $this->assertSame('C123456', $result->provider_id); + $this->assertSame('Detailed Component', $result->name); + $this->assertSame('Detailed description', $result->description); + $this->assertSame('Detailed Manufacturer', $result->manufacturer); $this->assertEquals('0603', $result->footprint); - $this->assertEquals('https://www.lcsc.com/product-detail/C123456.html', $result->provider_url); + $this->assertSame('https://www.lcsc.com/product-detail/C123456.html', $result->provider_url); $this->assertCount(1, $result->images); $this->assertCount(2, $result->parameters); $this->assertCount(1, $result->vendor_infos); @@ -465,8 +465,8 @@ class LCSCProviderTest extends TestCase $this->assertIsArray($result); $this->assertCount(1, $result); $this->assertInstanceOf(PurchaseInfoDTO::class, $result[0]); - $this->assertEquals('LCSC', $result[0]->distributor_name); - $this->assertEquals('C123456', $result[0]->order_number); + $this->assertSame('LCSC', $result[0]->distributor_name); + $this->assertSame('C123456', $result[0]->order_number); $this->assertCount(2, $result[0]->prices); } @@ -493,7 +493,7 @@ class LCSCProviderTest extends TestCase $this->httpClient->setResponseFactory([$mockResponse]); $result = $this->provider->getDetails('C123456'); - $this->assertEquals('Electronic Components -> Resistors (SMT)', $result->category); + $this->assertSame('Electronic Components -> Resistors (SMT)', $result->category); } public function testEmptyFootprintHandling(): void diff --git a/tests/Services/LabelSystem/BarcodeScanner/BarcodeScanHelperTest.php b/tests/Services/LabelSystem/BarcodeScanner/BarcodeScanHelperTest.php index fcea7730..248f1ae9 100644 --- a/tests/Services/LabelSystem/BarcodeScanner/BarcodeScanHelperTest.php +++ b/tests/Services/LabelSystem/BarcodeScanner/BarcodeScanHelperTest.php @@ -50,7 +50,7 @@ use App\Services\LabelSystem\BarcodeScanner\EIGP114BarcodeScanResult; use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class BarcodeScanHelperTest extends WebTestCase +final class BarcodeScanHelperTest extends WebTestCase { private ?BarcodeScanHelper $service = null; diff --git a/tests/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResultTest.php b/tests/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResultTest.php index 3a414997..6d69a773 100644 --- a/tests/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResultTest.php +++ b/tests/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResultTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Services\LabelSystem\BarcodeScanner; use App\Services\LabelSystem\BarcodeScanner\EIGP114BarcodeScanResult; use PHPUnit\Framework\TestCase; -class EIGP114BarcodeScanResultTest extends TestCase +final class EIGP114BarcodeScanResultTest extends TestCase { public function testGuessBarcodeVendor(): void diff --git a/tests/Services/LabelSystem/Barcodes/BarcodeContentGeneratorTest.php b/tests/Services/LabelSystem/Barcodes/BarcodeContentGeneratorTest.php index 69458734..d9185735 100644 --- a/tests/Services/LabelSystem/Barcodes/BarcodeContentGeneratorTest.php +++ b/tests/Services/LabelSystem/Barcodes/BarcodeContentGeneratorTest.php @@ -48,7 +48,7 @@ use App\Entity\Parts\StorageLocation; use App\Services\LabelSystem\Barcodes\BarcodeContentGenerator; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -class BarcodeContentGeneratorTest extends KernelTestCase +final class BarcodeContentGeneratorTest extends KernelTestCase { private ?object $service = null; diff --git a/tests/Services/LabelSystem/Barcodes/BarcodeHelperTest.php b/tests/Services/LabelSystem/Barcodes/BarcodeHelperTest.php index d681b3b9..e03221e5 100644 --- a/tests/Services/LabelSystem/Barcodes/BarcodeHelperTest.php +++ b/tests/Services/LabelSystem/Barcodes/BarcodeHelperTest.php @@ -27,7 +27,7 @@ use App\Services\LabelSystem\Barcodes\BarcodeHelper; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class BarcodeHelperTest extends WebTestCase +final class BarcodeHelperTest extends WebTestCase { protected ?BarcodeHelper $service = null; diff --git a/tests/Services/LabelSystem/LabelGeneratorTest.php b/tests/Services/LabelSystem/LabelGeneratorTest.php index 916d4317..5f6d8f04 100644 --- a/tests/Services/LabelSystem/LabelGeneratorTest.php +++ b/tests/Services/LabelSystem/LabelGeneratorTest.php @@ -51,7 +51,7 @@ use App\Entity\Parts\StorageLocation; use App\Services\LabelSystem\LabelGenerator; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class LabelGeneratorTest extends WebTestCase +final class LabelGeneratorTest extends WebTestCase { /** * @var LabelGenerator diff --git a/tests/Services/LabelSystem/LabelTextReplacerTest.php b/tests/Services/LabelSystem/LabelTextReplacerTest.php index 346d1bab..c4a140d8 100644 --- a/tests/Services/LabelSystem/LabelTextReplacerTest.php +++ b/tests/Services/LabelSystem/LabelTextReplacerTest.php @@ -47,7 +47,7 @@ use App\Entity\Parts\PartLot; use App\Services\LabelSystem\LabelTextReplacer; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class LabelTextReplacerTest extends WebTestCase +final class LabelTextReplacerTest extends WebTestCase { /** * @var LabelTextReplacer diff --git a/tests/Services/LabelSystem/PlaceholderProviders/AbstractElementProviderTest.php b/tests/Services/LabelSystem/PlaceholderProviders/AbstractElementProviderTest.php index fb917b82..584f708a 100644 --- a/tests/Services/LabelSystem/PlaceholderProviders/AbstractElementProviderTest.php +++ b/tests/Services/LabelSystem/PlaceholderProviders/AbstractElementProviderTest.php @@ -46,7 +46,7 @@ use App\Entity\Base\AbstractDBElement; use App\Services\LabelSystem\PlaceholderProviders\AbstractDBElementProvider; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class AbstractElementProviderTest extends WebTestCase +final class AbstractElementProviderTest extends WebTestCase { /** * @var AbstractDBElementProvider diff --git a/tests/Services/LabelSystem/PlaceholderProviders/GlobalProvidersTest.php b/tests/Services/LabelSystem/PlaceholderProviders/GlobalProvidersTest.php index d74bb215..80b6b76f 100644 --- a/tests/Services/LabelSystem/PlaceholderProviders/GlobalProvidersTest.php +++ b/tests/Services/LabelSystem/PlaceholderProviders/GlobalProvidersTest.php @@ -46,7 +46,7 @@ use App\Entity\Parts\Part; use App\Services\LabelSystem\PlaceholderProviders\GlobalProviders; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class GlobalProvidersTest extends WebTestCase +final class GlobalProvidersTest extends WebTestCase { /** * @var GlobalProviders diff --git a/tests/Services/LabelSystem/PlaceholderProviders/NamedElementProviderTest.php b/tests/Services/LabelSystem/PlaceholderProviders/NamedElementProviderTest.php index c5efc768..88b77e8e 100644 --- a/tests/Services/LabelSystem/PlaceholderProviders/NamedElementProviderTest.php +++ b/tests/Services/LabelSystem/PlaceholderProviders/NamedElementProviderTest.php @@ -46,7 +46,7 @@ use App\Entity\Contracts\NamedElementInterface; use App\Services\LabelSystem\PlaceholderProviders\NamedElementProvider; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class NamedElementProviderTest extends WebTestCase +final class NamedElementProviderTest extends WebTestCase { /** * @var NamedElementProvider diff --git a/tests/Services/LabelSystem/PlaceholderProviders/PartLotProviderTest.php b/tests/Services/LabelSystem/PlaceholderProviders/PartLotProviderTest.php index 5aa8f1bd..68425250 100644 --- a/tests/Services/LabelSystem/PlaceholderProviders/PartLotProviderTest.php +++ b/tests/Services/LabelSystem/PlaceholderProviders/PartLotProviderTest.php @@ -49,7 +49,7 @@ use App\Entity\UserSystem\User; use App\Services\LabelSystem\PlaceholderProviders\PartLotProvider; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class PartLotProviderTest extends WebTestCase +final class PartLotProviderTest extends WebTestCase { /** * @var PartLotProvider diff --git a/tests/Services/LabelSystem/PlaceholderProviders/PartProviderTest.php b/tests/Services/LabelSystem/PlaceholderProviders/PartProviderTest.php index 7af936cd..9f1c74f7 100644 --- a/tests/Services/LabelSystem/PlaceholderProviders/PartProviderTest.php +++ b/tests/Services/LabelSystem/PlaceholderProviders/PartProviderTest.php @@ -53,7 +53,7 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; #[Group('DB')] -class PartProviderTest extends WebTestCase +final class PartProviderTest extends WebTestCase { /** * @var PartProvider @@ -62,20 +62,15 @@ class PartProviderTest extends WebTestCase protected Part $target; - /** - * @var EntityManager - */ - protected $em; - protected function setUp(): void { self::bootKernel(); $this->service = self::getContainer()->get(PartProvider::class); $this->target = new Part(); - $this->em = self::getContainer()->get(EntityManagerInterface::class); + $em = self::getContainer()->get(EntityManagerInterface::class); - $this->target->setCategory($this->em->find(Category::class, 6)); - $this->target->setFootprint($this->em->find(Footprint::class, 6)); + $this->target->setCategory($em->find(Category::class, 6)); + $this->target->setFootprint($em->find(Footprint::class, 6)); $this->target->setManufacturer(null); $this->target->setMass(1234.2); diff --git a/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php b/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php index 6aa152b9..b5415131 100644 --- a/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php +++ b/tests/Services/LabelSystem/PlaceholderProviders/TimestampableElementProviderTest.php @@ -48,7 +48,7 @@ use App\Services\LabelSystem\PlaceholderProviders\TimestampableElementProvider; use DateTime; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class TimestampableElementProviderTest extends WebTestCase +final class TimestampableElementProviderTest extends WebTestCase { /** * @var GlobalProviders diff --git a/tests/Services/LabelSystem/SandboxedTwigFactoryTest.php b/tests/Services/LabelSystem/SandboxedTwigFactoryTest.php index 32317435..f10ef333 100644 --- a/tests/Services/LabelSystem/SandboxedTwigFactoryTest.php +++ b/tests/Services/LabelSystem/SandboxedTwigFactoryTest.php @@ -52,7 +52,7 @@ use App\Services\LabelSystem\SandboxedTwigFactory; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Twig\Sandbox\SecurityError; -class SandboxedTwigFactoryTest extends WebTestCase +final class SandboxedTwigFactoryTest extends WebTestCase { private ?SandboxedTwigFactory $service = null; diff --git a/tests/Services/LogSystem/EventCommentHelperTest.php b/tests/Services/LogSystem/EventCommentHelperTest.php index 9c78d4c6..616c1ddf 100644 --- a/tests/Services/LogSystem/EventCommentHelperTest.php +++ b/tests/Services/LogSystem/EventCommentHelperTest.php @@ -44,7 +44,7 @@ namespace App\Tests\Services\LogSystem; use App\Services\LogSystem\EventCommentHelper; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class EventCommentHelperTest extends WebTestCase +final class EventCommentHelperTest extends WebTestCase { /** * @var EventCommentHelper diff --git a/tests/Services/LogSystem/EventCommentNeededHelperTest.php b/tests/Services/LogSystem/EventCommentNeededHelperTest.php index 2eb2aceb..3ef238c5 100644 --- a/tests/Services/LogSystem/EventCommentNeededHelperTest.php +++ b/tests/Services/LogSystem/EventCommentNeededHelperTest.php @@ -28,7 +28,7 @@ use App\Settings\SystemSettings\HistorySettings; use App\Tests\SettingsTestHelper; use PHPUnit\Framework\TestCase; -class EventCommentNeededHelperTest extends TestCase +final class EventCommentNeededHelperTest extends TestCase { public function testIsCommentNeeded(): void { diff --git a/tests/Services/LogSystem/EventLoggerTest.php b/tests/Services/LogSystem/EventLoggerTest.php index 0dbb85a3..69870ac8 100644 --- a/tests/Services/LogSystem/EventLoggerTest.php +++ b/tests/Services/LogSystem/EventLoggerTest.php @@ -48,7 +48,7 @@ use App\Entity\LogSystem\UserLogoutLogEntry; use App\Services\LogSystem\EventLogger; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class EventLoggerTest extends WebTestCase +final class EventLoggerTest extends WebTestCase { /** * @var EventLogger diff --git a/tests/Services/LogSystem/TimeTravelTest.php b/tests/Services/LogSystem/TimeTravelTest.php index f0068778..9b51592d 100644 --- a/tests/Services/LogSystem/TimeTravelTest.php +++ b/tests/Services/LogSystem/TimeTravelTest.php @@ -29,7 +29,7 @@ use Doctrine\ORM\EntityManagerInterface; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -class TimeTravelTest extends KernelTestCase +final class TimeTravelTest extends KernelTestCase { private TimeTravel $service; diff --git a/tests/Services/Misc/FAIconGeneratorTest.php b/tests/Services/Misc/FAIconGeneratorTest.php index 1aec5d02..445c167c 100644 --- a/tests/Services/Misc/FAIconGeneratorTest.php +++ b/tests/Services/Misc/FAIconGeneratorTest.php @@ -26,7 +26,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\Services\Misc\FAIconGenerator; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class FAIconGeneratorTest extends WebTestCase +final class FAIconGeneratorTest extends WebTestCase { /** * @var FAIconGenerator diff --git a/tests/Services/Misc/MySQLDumpXMLConverterTest.php b/tests/Services/Misc/MySQLDumpXMLConverterTest.php index 98614b4b..a56083d8 100644 --- a/tests/Services/Misc/MySQLDumpXMLConverterTest.php +++ b/tests/Services/Misc/MySQLDumpXMLConverterTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Services\Misc; use App\Services\ImportExportSystem\PartKeeprImporter\MySQLDumpXMLConverter; use PHPUnit\Framework\TestCase; -class MySQLDumpXMLConverterTest extends TestCase +final class MySQLDumpXMLConverterTest extends TestCase { public function testConvertMySQLDumpXMLDataToArrayStructure(): void diff --git a/tests/Services/Misc/RangeParserTest.php b/tests/Services/Misc/RangeParserTest.php index 084ca80b..894034be 100644 --- a/tests/Services/Misc/RangeParserTest.php +++ b/tests/Services/Misc/RangeParserTest.php @@ -45,7 +45,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use App\Services\Misc\RangeParser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class RangeParserTest extends WebTestCase +final class RangeParserTest extends WebTestCase { /** * @var RangeParser diff --git a/tests/Services/Parameters/ParameterExtractorTest.php b/tests/Services/Parameters/ParameterExtractorTest.php index d0b8fed0..353a0697 100644 --- a/tests/Services/Parameters/ParameterExtractorTest.php +++ b/tests/Services/Parameters/ParameterExtractorTest.php @@ -46,7 +46,7 @@ use App\Entity\Parameters\AbstractParameter; use App\Services\Parameters\ParameterExtractor; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class ParameterExtractorTest extends WebTestCase +final class ParameterExtractorTest extends WebTestCase { protected $service; diff --git a/tests/Services/Parts/PartLotWithdrawAddHelperTest.php b/tests/Services/Parts/PartLotWithdrawAddHelperTest.php index 697d3983..de684094 100644 --- a/tests/Services/Parts/PartLotWithdrawAddHelperTest.php +++ b/tests/Services/Parts/PartLotWithdrawAddHelperTest.php @@ -18,7 +18,7 @@ class TestPartLot extends PartLot } } -class PartLotWithdrawAddHelperTest extends WebTestCase +final class PartLotWithdrawAddHelperTest extends WebTestCase { /** @@ -154,4 +154,19 @@ class PartLotWithdrawAddHelperTest extends WebTestCase $this->assertEqualsWithDelta(5.0, $this->partLot2->getAmount(), PHP_FLOAT_EPSILON); $this->assertEqualsWithDelta(2.0, $this->partLot3->getAmount(), PHP_FLOAT_EPSILON); } + + public function testStocktake(): void + { + //Stocktake lot 1 to 20 + $this->service->stocktake($this->partLot1, 20, "Test"); + $this->assertEqualsWithDelta(20.0, $this->partLot1->getAmount(), PHP_FLOAT_EPSILON); + $this->assertNotNull($this->partLot1->getLastStocktakeAt()); //Stocktake date should be set + + //Stocktake lot 2 to 5 + $this->partLot2->setInstockUnknown(true); + $this->service->stocktake($this->partLot2, 0, "Test"); + $this->assertEqualsWithDelta(0.0, $this->partLot2->getAmount(), PHP_FLOAT_EPSILON); + $this->assertFalse($this->partLot2->isInstockUnknown()); //Instock unknown should be cleared + + } } diff --git a/tests/Services/Parts/PartsTableActionHandlerTest.php b/tests/Services/Parts/PartsTableActionHandlerTest.php index c5105cd7..1772195e 100644 --- a/tests/Services/Parts/PartsTableActionHandlerTest.php +++ b/tests/Services/Parts/PartsTableActionHandlerTest.php @@ -27,7 +27,7 @@ use App\Services\Parts\PartsTableActionHandler; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\RedirectResponse; -class PartsTableActionHandlerTest extends WebTestCase +final class PartsTableActionHandlerTest extends WebTestCase { private PartsTableActionHandler $service; diff --git a/tests/Services/Parts/PricedetailHelperTest.php b/tests/Services/Parts/PricedetailHelperTest.php index 5d9bd351..08a5d6dd 100644 --- a/tests/Services/Parts/PricedetailHelperTest.php +++ b/tests/Services/Parts/PricedetailHelperTest.php @@ -30,7 +30,7 @@ use App\Services\Formatters\AmountFormatter; use App\Services\Parts\PricedetailHelper; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class PricedetailHelperTest extends WebTestCase +final class PricedetailHelperTest extends WebTestCase { /** * @var AmountFormatter diff --git a/tests/Services/ProjectSystem/ProjectBuildHelperTest.php b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php index 5009f849..fb31b51e 100644 --- a/tests/Services/ProjectSystem/ProjectBuildHelperTest.php +++ b/tests/Services/ProjectSystem/ProjectBuildHelperTest.php @@ -29,7 +29,7 @@ use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Services\ProjectSystem\ProjectBuildHelper; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class ProjectBuildHelperTest extends WebTestCase +final class ProjectBuildHelperTest extends WebTestCase { /** @var ProjectBuildHelper */ protected $service; diff --git a/tests/Services/ProjectSystem/ProjectBuildPartHelperTest.php b/tests/Services/ProjectSystem/ProjectBuildPartHelperTest.php index 4baa7cf3..894f6315 100644 --- a/tests/Services/ProjectSystem/ProjectBuildPartHelperTest.php +++ b/tests/Services/ProjectSystem/ProjectBuildPartHelperTest.php @@ -26,7 +26,7 @@ use App\Entity\ProjectSystem\Project; use App\Services\ProjectSystem\ProjectBuildPartHelper; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class ProjectBuildPartHelperTest extends WebTestCase +final class ProjectBuildPartHelperTest extends WebTestCase { /** @var ProjectBuildPartHelper */ protected $service; diff --git a/tests/Services/System/BackupManagerTest.php b/tests/Services/System/BackupManagerTest.php index 145b039d..f75ef8f3 100644 --- a/tests/Services/System/BackupManagerTest.php +++ b/tests/Services/System/BackupManagerTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Services\System; use App\Services\System\BackupManager; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -class BackupManagerTest extends KernelTestCase +final class BackupManagerTest extends KernelTestCase { private ?BackupManager $backupManager = null; @@ -77,9 +77,9 @@ class BackupManagerTest extends KernelTestCase $result = preg_match('/pre-update-v([\d.]+)-to-v?([\d.]+)-/', $filename, $matches); - $this->assertEquals(1, $result); - $this->assertEquals('2.5.1', $matches[1]); - $this->assertEquals('2.6.0', $matches[2]); + $this->assertSame(1, $result); + $this->assertSame('2.5.1', $matches[1]); + $this->assertSame('2.6.0', $matches[2]); } /** @@ -90,13 +90,13 @@ class BackupManagerTest extends KernelTestCase // Without 'v' prefix on target version $filename1 = 'pre-update-v1.0.0-to-2.0.0-2024-01-30-185400.zip'; preg_match('/pre-update-v([\d.]+)-to-v?([\d.]+)-/', $filename1, $matches1); - $this->assertEquals('1.0.0', $matches1[1]); - $this->assertEquals('2.0.0', $matches1[2]); + $this->assertSame('1.0.0', $matches1[1]); + $this->assertSame('2.0.0', $matches1[2]); // With 'v' prefix on target version $filename2 = 'pre-update-v1.0.0-to-v2.0.0-2024-01-30-185400.zip'; preg_match('/pre-update-v([\d.]+)-to-v?([\d.]+)-/', $filename2, $matches2); - $this->assertEquals('1.0.0', $matches2[1]); - $this->assertEquals('2.0.0', $matches2[2]); + $this->assertSame('1.0.0', $matches2[1]); + $this->assertSame('2.0.0', $matches2[2]); } } diff --git a/tests/Services/System/UpdateExecutorTest.php b/tests/Services/System/UpdateExecutorTest.php index 851d060c..48cddf8d 100644 --- a/tests/Services/System/UpdateExecutorTest.php +++ b/tests/Services/System/UpdateExecutorTest.php @@ -25,7 +25,7 @@ namespace App\Tests\Services\System; use App\Services\System\UpdateExecutor; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -class UpdateExecutorTest extends KernelTestCase +final class UpdateExecutorTest extends KernelTestCase { private ?UpdateExecutor $updateExecutor = null; diff --git a/tests/Services/Trees/NodesListBuilderTest.php b/tests/Services/Trees/NodesListBuilderTest.php index 8f4bf23b..d314114d 100644 --- a/tests/Services/Trees/NodesListBuilderTest.php +++ b/tests/Services/Trees/NodesListBuilderTest.php @@ -30,7 +30,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; /** * @Group DB */ -class NodesListBuilderTest extends WebTestCase +final class NodesListBuilderTest extends WebTestCase { protected $em; /** diff --git a/tests/Services/Trees/TreeViewGeneratorTest.php b/tests/Services/Trees/TreeViewGeneratorTest.php index ebec94d6..190f1068 100644 --- a/tests/Services/Trees/TreeViewGeneratorTest.php +++ b/tests/Services/Trees/TreeViewGeneratorTest.php @@ -31,7 +31,7 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; #[Group('DB')] -class TreeViewGeneratorTest extends WebTestCase +final class TreeViewGeneratorTest extends WebTestCase { protected $em; /** diff --git a/tests/Services/UserSystem/PermissionManagerTest.php b/tests/Services/UserSystem/PermissionManagerTest.php index 478202f4..e6da72d4 100644 --- a/tests/Services/UserSystem/PermissionManagerTest.php +++ b/tests/Services/UserSystem/PermissionManagerTest.php @@ -30,7 +30,7 @@ use App\Services\UserSystem\PermissionManager; use InvalidArgumentException; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class PermissionManagerTest extends WebTestCase +final class PermissionManagerTest extends WebTestCase { protected ?User $user_withoutGroup = null; diff --git a/tests/Services/UserSystem/PermissionSchemaUpdaterTest.php b/tests/Services/UserSystem/PermissionSchemaUpdaterTest.php index 1e41474c..738ff649 100644 --- a/tests/Services/UserSystem/PermissionSchemaUpdaterTest.php +++ b/tests/Services/UserSystem/PermissionSchemaUpdaterTest.php @@ -39,7 +39,7 @@ class TestPermissionHolder implements HasPermissionsInterface } } -class PermissionSchemaUpdaterTest extends WebTestCase +final class PermissionSchemaUpdaterTest extends WebTestCase { /** * @var PermissionSchemaUpdater diff --git a/tests/Services/UserSystem/TFA/BackupCodeGeneratorTest.php b/tests/Services/UserSystem/TFA/BackupCodeGeneratorTest.php index 2b6c22d1..5a54f2f3 100644 --- a/tests/Services/UserSystem/TFA/BackupCodeGeneratorTest.php +++ b/tests/Services/UserSystem/TFA/BackupCodeGeneratorTest.php @@ -27,7 +27,7 @@ use App\Services\UserSystem\TFA\BackupCodeGenerator; use PHPUnit\Framework\TestCase; use RuntimeException; -class BackupCodeGeneratorTest extends TestCase +final class BackupCodeGeneratorTest extends TestCase { /** * Test if an exception is thrown if you are using a too high code length. diff --git a/tests/Services/UserSystem/TFA/BackupCodeManagerTest.php b/tests/Services/UserSystem/TFA/BackupCodeManagerTest.php index 35b7f4f8..4b1a0496 100644 --- a/tests/Services/UserSystem/TFA/BackupCodeManagerTest.php +++ b/tests/Services/UserSystem/TFA/BackupCodeManagerTest.php @@ -26,7 +26,7 @@ use App\Entity\UserSystem\User; use App\Services\UserSystem\TFA\BackupCodeManager; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class BackupCodeManagerTest extends WebTestCase +final class BackupCodeManagerTest extends WebTestCase { /** * @var BackupCodeManager diff --git a/tests/Services/UserSystem/VoterHelperTest.php b/tests/Services/UserSystem/VoterHelperTest.php index 53d7ee82..3f21c9ab 100644 --- a/tests/Services/UserSystem/VoterHelperTest.php +++ b/tests/Services/UserSystem/VoterHelperTest.php @@ -34,7 +34,7 @@ use Symfony\Component\Security\Core\Authentication\Token\NullToken; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken; -class VoterHelperTest extends KernelTestCase +final class VoterHelperTest extends KernelTestCase { protected ?VoterHelper $service = null; diff --git a/tests/Settings/SynonymSettingsTest.php b/tests/Settings/SynonymSettingsTest.php index 2d1407ac..00e5be4c 100644 --- a/tests/Settings/SynonymSettingsTest.php +++ b/tests/Settings/SynonymSettingsTest.php @@ -1,4 +1,7 @@ . */ - namespace App\Tests\Settings; use App\Services\ElementTypes; @@ -25,7 +27,7 @@ use App\Settings\SynonymSettings; use App\Tests\SettingsTestHelper; use PHPUnit\Framework\TestCase; -class SynonymSettingsTest extends TestCase +final class SynonymSettingsTest extends TestCase { public function testGetSingularSynonymForType(): void diff --git a/tests/Twig/EntityExtensionTest.php b/tests/Twig/EntityExtensionTest.php index 18fe970b..d206b941 100644 --- a/tests/Twig/EntityExtensionTest.php +++ b/tests/Twig/EntityExtensionTest.php @@ -39,7 +39,7 @@ use App\Entity\UserSystem\User; use App\Twig\EntityExtension; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class EntityExtensionTest extends WebTestCase +final class EntityExtensionTest extends WebTestCase { /** @var EntityExtension */ protected $service; @@ -55,20 +55,20 @@ class EntityExtensionTest extends WebTestCase public function testGetEntityType(): void { - $this->assertSame('part', $this->service->getEntityType(new Part())); - $this->assertSame('footprint', $this->service->getEntityType(new Footprint())); - $this->assertSame('storelocation', $this->service->getEntityType(new StorageLocation())); - $this->assertSame('manufacturer', $this->service->getEntityType(new Manufacturer())); - $this->assertSame('category', $this->service->getEntityType(new Category())); - $this->assertSame('device', $this->service->getEntityType(new Project())); - $this->assertSame('attachment', $this->service->getEntityType(new PartAttachment())); - $this->assertSame('supplier', $this->service->getEntityType(new Supplier())); - $this->assertSame('user', $this->service->getEntityType(new User())); - $this->assertSame('group', $this->service->getEntityType(new Group())); - $this->assertSame('currency', $this->service->getEntityType(new Currency())); - $this->assertSame('measurement_unit', $this->service->getEntityType(new MeasurementUnit())); - $this->assertSame('label_profile', $this->service->getEntityType(new LabelProfile())); - $this->assertSame('part_custom_state', $this->service->getEntityType(new PartCustomState())); + $this->assertSame('part', $this->service->entityType(new Part())); + $this->assertSame('footprint', $this->service->entityType(new Footprint())); + $this->assertSame('storelocation', $this->service->entityType(new StorageLocation())); + $this->assertSame('manufacturer', $this->service->entityType(new Manufacturer())); + $this->assertSame('category', $this->service->entityType(new Category())); + $this->assertSame('device', $this->service->entityType(new Project())); + $this->assertSame('attachment', $this->service->entityType(new PartAttachment())); + $this->assertSame('supplier', $this->service->entityType(new Supplier())); + $this->assertSame('user', $this->service->entityType(new User())); + $this->assertSame('group', $this->service->entityType(new Group())); + $this->assertSame('currency', $this->service->entityType(new Currency())); + $this->assertSame('measurement_unit', $this->service->entityType(new MeasurementUnit())); + $this->assertSame('label_profile', $this->service->entityType(new LabelProfile())); + $this->assertSame('part_custom_state', $this->service->entityType(new PartCustomState())); } } diff --git a/tests/Twig/TwigCoreExtensionTest.php b/tests/Twig/TwigCoreExtensionTest.php index 1aa1f7ca..be8ced04 100644 --- a/tests/Twig/TwigCoreExtensionTest.php +++ b/tests/Twig/TwigCoreExtensionTest.php @@ -26,7 +26,7 @@ use App\Twig\TwigCoreExtension; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class TwigCoreExtensionTest extends WebTestCase +final class TwigCoreExtensionTest extends WebTestCase { /** @var TwigCoreExtension */ protected $service; diff --git a/tests/Twig/UserExtensionTest.php b/tests/Twig/UserExtensionTest.php index 235d39c1..f8422a2c 100644 --- a/tests/Twig/UserExtensionTest.php +++ b/tests/Twig/UserExtensionTest.php @@ -27,7 +27,7 @@ use App\Twig\UserExtension; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; -class UserExtensionTest extends WebTestCase +final class UserExtensionTest extends WebTestCase { protected $service; diff --git a/tests/Validator/Constraints/NoneOfItsChildrenValidatorTest.php b/tests/Validator/Constraints/NoneOfItsChildrenValidatorTest.php index 0efcd5de..e4b721ca 100644 --- a/tests/Validator/Constraints/NoneOfItsChildrenValidatorTest.php +++ b/tests/Validator/Constraints/NoneOfItsChildrenValidatorTest.php @@ -28,15 +28,12 @@ use App\Validator\Constraints\NoneOfItsChildren; use App\Validator\Constraints\NoneOfItsChildrenValidator; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; -class NoneOfItsChildrenValidatorTest extends ConstraintValidatorTestCase +final class NoneOfItsChildrenValidatorTest extends ConstraintValidatorTestCase { protected AttachmentType $root_node; protected AttachmentType $child1; - protected AttachmentType $child2; - protected AttachmentType $child3; protected AttachmentType $child1_1; - protected AttachmentType $child1_2; protected function setUp(): void { @@ -49,14 +46,14 @@ class NoneOfItsChildrenValidatorTest extends ConstraintValidatorTestCase $this->root_node->setName('root')->setParent(null); $this->child1 = new AttachmentType(); $this->child1->setParent($this->root_node)->setName('child1'); - $this->child2 = new AttachmentType(); - $this->child2->setName('child2')->setParent($this->root_node); - $this->child3 = new AttachmentType(); - $this->child3->setName('child3')->setParent($this->root_node); + $child2 = new AttachmentType(); + $child2->setName('child2')->setParent($this->root_node); + $child3 = new AttachmentType(); + $child3->setName('child3')->setParent($this->root_node); $this->child1_1 = new AttachmentType(); $this->child1_1->setName('child1_1')->setParent($this->child1); - $this->child1_2 = new AttachmentType(); - $this->child1_2->setName('child1_2')->setParent($this->child1); + $child1_2 = new AttachmentType(); + $child1_2->setName('child1_2')->setParent($this->child1); } diff --git a/tests/Validator/Constraints/SelectableValidatorTest.php b/tests/Validator/Constraints/SelectableValidatorTest.php index bc520621..68f36f87 100644 --- a/tests/Validator/Constraints/SelectableValidatorTest.php +++ b/tests/Validator/Constraints/SelectableValidatorTest.php @@ -29,7 +29,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; -class SelectableValidatorTest extends ConstraintValidatorTestCase +final class SelectableValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): SelectableValidator { diff --git a/tests/Validator/Constraints/UniqueObjectCollectionValidatorTest.php b/tests/Validator/Constraints/UniqueObjectCollectionValidatorTest.php index d9fab6cf..3863d604 100644 --- a/tests/Validator/Constraints/UniqueObjectCollectionValidatorTest.php +++ b/tests/Validator/Constraints/UniqueObjectCollectionValidatorTest.php @@ -30,7 +30,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; -class UniqueObjectCollectionValidatorTest extends ConstraintValidatorTestCase +final class UniqueObjectCollectionValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): UniqueObjectCollectionValidator { diff --git a/tests/Validator/Constraints/UrlOrBuiltinValidatorTest.php b/tests/Validator/Constraints/UrlOrBuiltinValidatorTest.php index c75754df..326809df 100644 --- a/tests/Validator/Constraints/UrlOrBuiltinValidatorTest.php +++ b/tests/Validator/Constraints/UrlOrBuiltinValidatorTest.php @@ -27,7 +27,7 @@ use App\Validator\Constraints\UrlOrBuiltinValidator; use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; -class UrlOrBuiltinValidatorTest extends ConstraintValidatorTestCase +final class UrlOrBuiltinValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): UrlOrBuiltinValidator diff --git a/tests/Validator/Constraints/ValidGTINValidatorTest.php b/tests/Validator/Constraints/ValidGTINValidatorTest.php new file mode 100644 index 00000000..6b01519b --- /dev/null +++ b/tests/Validator/Constraints/ValidGTINValidatorTest.php @@ -0,0 +1,75 @@ +. + */ +namespace App\Tests\Validator\Constraints; + +use App\Validator\Constraints\ValidGTIN; +use App\Validator\Constraints\ValidGTINValidator; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\ConstraintValidatorInterface; +use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; + +final class ValidGTINValidatorTest extends ConstraintValidatorTestCase +{ + + public function testAllowNull(): void + { + $this->validator->validate(null, new ValidGTIN()); + $this->assertNoViolation(); + } + + public function testValidGTIN8(): void + { + $this->validator->validate('12345670', new ValidGTIN()); + $this->assertNoViolation(); + } + + public function testValidGTIN12(): void + { + $this->validator->validate('123456789012', new ValidGTIN()); + $this->assertNoViolation(); + } + + public function testValidGTIN13(): void + { + $this->validator->validate('1234567890128', new ValidGTIN()); + $this->assertNoViolation(); + } + + public function testValidGTIN14(): void + { + $this->validator->validate('12345678901231', new ValidGTIN()); + $this->assertNoViolation(); + } + + public function testInvalidGTIN(): void + { + $this->validator->validate('1234567890123', new ValidGTIN()); + $this->buildViolation('validator.invalid_gtin') + ->assertRaised(); + } + + protected function createValidator(): ConstraintValidatorInterface + { + return new ValidGTINValidator(); + } +} diff --git a/tests/Validator/Constraints/ValidGoogleAuthCodeValidatorTest.php b/tests/Validator/Constraints/ValidGoogleAuthCodeValidatorTest.php index 6eb9270e..4924f34b 100644 --- a/tests/Validator/Constraints/ValidGoogleAuthCodeValidatorTest.php +++ b/tests/Validator/Constraints/ValidGoogleAuthCodeValidatorTest.php @@ -34,7 +34,7 @@ use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; -class ValidGoogleAuthCodeValidatorTest extends ConstraintValidatorTestCase +final class ValidGoogleAuthCodeValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): ConstraintValidatorInterface diff --git a/tests/Validator/Constraints/ValidThemeValidatorTest.php b/tests/Validator/Constraints/ValidThemeValidatorTest.php index 9db8f33b..50b11820 100644 --- a/tests/Validator/Constraints/ValidThemeValidatorTest.php +++ b/tests/Validator/Constraints/ValidThemeValidatorTest.php @@ -27,7 +27,7 @@ use App\Validator\Constraints\ValidThemeValidator; use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; -class ValidThemeValidatorTest extends ConstraintValidatorTestCase +final class ValidThemeValidatorTest extends ConstraintValidatorTestCase { protected function createValidator(): ValidThemeValidator diff --git a/translations/frontend.da.xlf b/translations/frontend.da.xlf index 9c6b3129..4b6a15b9 100644 --- a/translations/frontend.da.xlf +++ b/translations/frontend.da.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/frontend.de.xlf b/translations/frontend.de.xlf index 4eaded60..9ebd0d32 100644 --- a/translations/frontend.de.xlf +++ b/translations/frontend.de.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/frontend.en.xlf b/translations/frontend.en.xlf index aa3cf2d9..91617f79 100644 --- a/translations/frontend.en.xlf +++ b/translations/frontend.en.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/frontend.hu.xlf b/translations/frontend.hu.xlf index bdcda170..c303dedc 100644 --- a/translations/frontend.hu.xlf +++ b/translations/frontend.hu.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/frontend.uk.xlf b/translations/frontend.uk.xlf index 86f51f95..fee1b03e 100644 --- a/translations/frontend.uk.xlf +++ b/translations/frontend.uk.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/frontend.zh.xlf b/translations/frontend.zh.xlf index d2ea6fd0..8bb063b8 100644 --- a/translations/frontend.zh.xlf +++ b/translations/frontend.zh.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index c4f2d5d8..c20e8152 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -11868,7 +11868,7 @@ Buerklin-API-Authentication-Server: update_manager.view_release - update_manager.view_release + Release ansehen @@ -11964,7 +11964,7 @@ Buerklin-API-Authentication-Server: update_manager.view_release_notes - update_manager.view_release_notes + Release notes ansehen @@ -12102,7 +12102,7 @@ Buerklin-API-Authentication-Server: perm.system.manage_updates - perm.system.manage_updates + Part-DB Updated verwalten @@ -12354,13 +12354,13 @@ Buerklin-API-Authentication-Server: settings.ips.generic_web_provider.enabled.help - settings.ips.generic_web_provider.enabled.help + Wenn der Anbieter aktiviert ist, können Benutzer im Namen des Part-DB-Servers Anfragen an beliebige Websites stellen. Aktivieren Sie diese Option nur, wenn Sie sich der möglichen Folgen bewusst sind. info_providers.from_url.title - Erstelle [part] aus URL + Erstelle [Part] aus URL @@ -12399,5 +12399,113 @@ Buerklin-API-Authentication-Server: Update zu + + + part.gtin + GTIN / EAN + + + + + info_providers.capabilities.gtin + GTIN / EAN + + + + + part.table.gtin + GTIN + + + + + scan_dialog.mode.gtin + GTIN / EAN Barcode + + + + + attachment_type.edit.allowed_targets + Nur verwenden für + + + + + attachment_type.edit.allowed_targets.help + Machen Sie diesen Anhangstyp nur für bestimmte Elementtypen verfügbar. Leer lassen, um diesen Anhangstyp für alle Elementtypen anzuzeigen. + + + + + orderdetails.edit.prices_includes_vat + Preise einschl. MwSt. + + + + + prices.incl_vat + Inkl. MwSt. + + + + + prices.excl_vat + Exkl. MwSt. + + + + + settings.system.localization.prices_include_tax_by_default + Preise enthalten standardmäßig Mehrwertsteuer + + + + + settings.system.localization.prices_include_tax_by_default.description + Der Standardwert für neu erstellte Einkaufinformationen, ob die Preise Mehrwertsteuer enthalten oder nicht. + + + + + part_lot.edit.last_stocktake_at + Letzte Inventur + + + + + perm.parts_stock.stocktake + Inventur + + + + + part.info.stocktake_modal.title + Inventur des Bestandes + + + + + part.info.stocktake_modal.expected_amount + Erwartete Menge + + + + + part.info.stocktake_modal.actual_amount + Tatsächliche Menge + + + + + log.part_stock_changed.stock_take + Inventur + + + + + log.element_edited.changed_fields.last_stocktake_at + Letzte Inventur + + - \ No newline at end of file + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 5692ed25..d9418563 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12401,5 +12401,113 @@ Buerklin-API Authentication server: Update to + + + part.gtin + GTIN / EAN + + + + + info_providers.capabilities.gtin + GTIN / EAN + + + + + part.table.gtin + GTIN + + + + + scan_dialog.mode.gtin + GTIN / EAN barcode + + + + + attachment_type.edit.allowed_targets + Use only for + + + + + attachment_type.edit.allowed_targets.help + Make this attachment type only available for certain element classes. Leave empty to show this attachment type for all element classes. + + + + + orderdetails.edit.prices_includes_vat + Prices include VAT + + + + + prices.incl_vat + Incl. VAT + + + + + prices.excl_vat + Excl. VAT + + + + + settings.system.localization.prices_include_tax_by_default + Prices include VAT by default + + + + + settings.system.localization.prices_include_tax_by_default.description + The default value for newly created purchase infos, if prices include VAT or not. + + + + + part_lot.edit.last_stocktake_at + Last stocktake + + + + + perm.parts_stock.stocktake + Stocktake + + + + + part.info.stocktake_modal.title + Stocktake lot + + + + + part.info.stocktake_modal.expected_amount + Expected amount + + + + + part.info.stocktake_modal.actual_amount + Actual amount + + + + + log.part_stock_changed.stock_take + Stocktake + + + + + log.element_edited.changed_fields.last_stocktake_at + Last stocktake + + - \ No newline at end of file + diff --git a/translations/security.da.xlf b/translations/security.da.xlf index 99329533..ab35c605 100644 --- a/translations/security.da.xlf +++ b/translations/security.da.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.de.xlf b/translations/security.de.xlf index 2a357094..927f8f9c 100644 --- a/translations/security.de.xlf +++ b/translations/security.de.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.en.xlf b/translations/security.en.xlf index 5a79d6ec..0b0b4569 100644 --- a/translations/security.en.xlf +++ b/translations/security.en.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.hu.xlf b/translations/security.hu.xlf index 3c885815..7c448da0 100644 --- a/translations/security.hu.xlf +++ b/translations/security.hu.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.nl.xlf b/translations/security.nl.xlf index 0e4ecc41..7ba9fcc1 100644 --- a/translations/security.nl.xlf +++ b/translations/security.nl.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.uk.xlf b/translations/security.uk.xlf index 03be9410..12737cf3 100644 --- a/translations/security.uk.xlf +++ b/translations/security.uk.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.zh.xlf b/translations/security.zh.xlf index 181c9c0f..58fbb26f 100644 --- a/translations/security.zh.xlf +++ b/translations/security.zh.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index e2e70d03..624c6a89 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -247,5 +247,11 @@ There is already a translation defined for this type and language! + + + validator.invalid_gtin + This is not an valid GTIN / EAN! + + - \ No newline at end of file + diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 03942667..e5392d76 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -148,7 +148,7 @@ project.bom_has_to_include_all_subelement_parts - BOM projektu musi zawierać wszystkie komponenty produkcyjne podprojektów. Brakuje komponentu %part_name% projektu %project_name%! + BOM projektu musi zawierać wszystkie komponenty produkcyjne pod projektów @@ -223,6 +223,12 @@ Ze względu na ograniczenia techniczne nie jest możliwe wybranie daty po 19 stycznia 2038 w systemach 32-bitowych! + + + validator.fileSize.invalidFormat + Niewłaściwy format + + validator.invalid_range @@ -235,5 +241,11 @@ Nieprawidłowy kod. Sprawdź, czy aplikacja uwierzytelniająca jest poprawnie skonfigurowana i czy zarówno serwer, jak i urządzenie uwierzytelniające mają poprawnie ustawiony czas. + + + settings.synonyms.type_synonyms.collection_type.duplicate + Duplikuj + + - \ No newline at end of file + diff --git a/yarn.lock b/yarn.lock index 6bcc4032..e3d72ad7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -837,159 +837,160 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" -"@ckeditor/ckeditor5-adapter-ckfinder@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-adapter-ckfinder/-/ckeditor5-adapter-ckfinder-47.4.0.tgz#7ff01dc465a8cd71f7a1acd0e9f943649ffce9df" - integrity sha512-g90RXXOoyBL0hsUMo6/IsCKF6qlKtxYlwzeTch+XboZOxkvJmozETKY4mnkR+XI1xZeO1bqqzLe8sKiFRvG7Hg== +"@ckeditor/ckeditor5-adapter-ckfinder@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-adapter-ckfinder/-/ckeditor5-adapter-ckfinder-47.5.0.tgz#76d6d289e7603da1ff209210a53fefbe3e5d114c" + integrity sha512-fOAmkBcSIWrGFDoz7kdb9XE/8yU7MnmzJ5JNbDVGNnpX+IL/1xRwvsnipwJzvJn8i3vo25kbyKLrh7FA8CsFtA== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-upload" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-upload" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-alignment@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-alignment/-/ckeditor5-alignment-47.4.0.tgz#a0d4fc432e1a8bcc15255cd383fbaf9ca2c37642" - integrity sha512-MI4PrumF62HZ5kG824WOhqtntDS6oPhmlFwg2vOd8L8fW1Gn4SgigvhqxARLi/OIf0ExnNcXFunS30B6lz1Ciw== +"@ckeditor/ckeditor5-alignment@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-alignment/-/ckeditor5-alignment-47.5.0.tgz#e808468e40b7abf9b31f3d4ba4711cbc5deb22a3" + integrity sha512-XI+olNUE92MmD4EMbhPhDmk63wt/b+QGNssH0kG/KjNJ/awoQIe+T9r4/k8WzMK7B2j4mdycgI62+ib5rH6XPw== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-autoformat@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-autoformat/-/ckeditor5-autoformat-47.4.0.tgz#1e14143970abd433ebfcc5b4b6ffcc65d86069fc" - integrity sha512-dYjPpSaIt8z8d7em+I54+S6Y0m/4fXX27DF6gXMHG+79TIzZxakHK096RJBxj3cIjpzSjHI+v9FQ1Y+nO/M79Q== +"@ckeditor/ckeditor5-autoformat@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-autoformat/-/ckeditor5-autoformat-47.5.0.tgz#c38a3e1a2e18b23e9b74d7ad4fa1c49d7bb3f72c" + integrity sha512-Z9f589prwjroiEJPLRNFqSzaALloOm3oPPUN4jH/YyyRnn1mv+7vI6TEPm7ZLKks3ztBN3yv1hFnrAd9oGqY+g== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-heading" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-heading" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-autosave@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-autosave/-/ckeditor5-autosave-47.4.0.tgz#e3d85027040d48eb46306c3fca0c0066b00e34a0" - integrity sha512-1DpjdGn+xXfYoeDd6SIcQbkUiOeHQbjN7qmjQWrd6JvowQ6loPtDPGL9OHmL4OFubrVn5GM4dS3E1+cU29SVHg== +"@ckeditor/ckeditor5-autosave@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-autosave/-/ckeditor5-autosave-47.5.0.tgz#ded3ffef4b96ad7a9576a709b512c165532444d1" + integrity sha512-KKtgL/uJdVLP2srSXG6MbYEZTCjzC2sy3kVcKtkyD6T+q3SA8OWsjH6jCLIPBZkDLNNedNnKnaeUR9+Na4i7Bg== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-basic-styles@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-basic-styles/-/ckeditor5-basic-styles-47.4.0.tgz#c277f33868f80e071a9761e187a2c95c3b444bf8" - integrity sha512-nCVP7W5ryshBG7UfXuFRv58qb/HmSS9Gjb2UUM84ODLOjYPFxvzWgQ5bV5t+x1bYAT8z/Xqfv9Ycs9ywEwOA9A== +"@ckeditor/ckeditor5-basic-styles@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-basic-styles/-/ckeditor5-basic-styles-47.5.0.tgz#f14399bfba8503c4d0284cd0a7590c5ee9b7ccf6" + integrity sha512-az6yy2hShx9TO9tk9oUEy4akO6V7CKl6d8R4PjGij+PxYeGbiHXSPRMjLrJkoRqj72okl+5S22YmhEf1KIpoPw== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-block-quote@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-block-quote/-/ckeditor5-block-quote-47.4.0.tgz#2288acbe34cb8e489e57b67192a0edc90510ab76" - integrity sha512-B1iX0p5ByU/y7AVREgevr0Kfobt9uT1n9rtXToXbA9W4u4yZIVJULpceTgDw+/OJNU8lyKbq/S/6trjYFsyf0Q== +"@ckeditor/ckeditor5-block-quote@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-block-quote/-/ckeditor5-block-quote-47.5.0.tgz#fafeff003cd6ac3c10b8c9ed88aba26c8a0a97c0" + integrity sha512-x2T+dn+2CU/r5hmun8AaVwSqNviaod/z483oh0XoexxXDcXE6wLr2FN591INLIdzO4/N2ez3AhcBt0focXYAbg== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-enter" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-enter" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-bookmark@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-bookmark/-/ckeditor5-bookmark-47.4.0.tgz#7cd5d324ad48992c8c4a5ad2b4a09bd07cb3b23e" - integrity sha512-XBAOfYpy0TdVqAXsBgKSKCD46S7kR/oohqP9UKTGUGrNjojW6FS1k1IxvcpRVATn0xPHjZld58wkwizIdeJveg== +"@ckeditor/ckeditor5-bookmark@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-bookmark/-/ckeditor5-bookmark-47.5.0.tgz#c559ead16e24b8b5b07243adbaa16d4271cb2296" + integrity sha512-5EkwJE5BuVTMHjw3VDKscVbMoG4kxD0wDCN2DTnhRo/drPNZ3q2Jw+3SJigpUrFWYUtzhF0s7+Mm6TL94rSBaA== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-link" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-widget" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-link" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-widget" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-ckbox@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ckbox/-/ckeditor5-ckbox-47.4.0.tgz#e9cdb256c318bfbb0263a23bfb25637f24923e81" - integrity sha512-Utk9nYwzVRLQXYVVR+oi3x4xN7C0lzt+ZUyPjBRf3k60ijP/OpA8lsJJWzonuEEsdELsLzaBNSivTa9hjLZLDA== +"@ckeditor/ckeditor5-ckbox@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ckbox/-/ckeditor5-ckbox-47.5.0.tgz#8d1efbd62e8c87509fe8796d64d79139c9d46e47" + integrity sha512-OvTjDZSU+XNNzEmny6X3D90lhGF14DsnS00TcYj7uRqiwOUtnWw0ba0Hw4ulJ9Jrfs2CzG+rz2YokMm5iMgQHw== dependencies: - "@ckeditor/ckeditor5-cloud-services" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-image" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-upload" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" + "@ckeditor/ckeditor5-cloud-services" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-image" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-upload" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" blurhash "2.0.5" - ckeditor5 "47.4.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-ckfinder@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ckfinder/-/ckeditor5-ckfinder-47.4.0.tgz#bde9e251ca17984fc8095bc1abdf8002d7b57e84" - integrity sha512-jXWwDfzFOn2S/oK84Io6cB7I0W9I7CwMyBfg5YbCEhYtv5aeNQBpRqwik/5cfmMrBMBXrPu1QRs60NIwegk/Eg== +"@ckeditor/ckeditor5-ckfinder@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ckfinder/-/ckeditor5-ckfinder-47.5.0.tgz#a0ba717932942c8bdac9e62d08bc411a87fd4e1c" + integrity sha512-QoyPTGypDiPgehwaaykzfH8ZUzr2qhHKj0BC7pqhAuHZaWzdCl49g2ZTI2PlMZRIrYNWr0io5zxQq/elSOgwcA== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-image" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-image" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-clipboard@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-clipboard/-/ckeditor5-clipboard-47.4.0.tgz#72558f987e559d91ddee17ce6594dbc96ade9fd0" - integrity sha512-LUR5yTXjHxLn8YLKrJj4/DBtqk6zdPg5SAVXkpNSz5UxU63aaj/L7jKCInr36Uy23Ov5TgT6FkgXPaBtakAqDA== +"@ckeditor/ckeditor5-clipboard@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-clipboard/-/ckeditor5-clipboard-47.5.0.tgz#aec63eb1d509e8476079cd7d6012611b469dad19" + integrity sha512-LB+KSXasEOL9/3OC2WBPwbLaKd+tDkix+4RVaki9hciQb4XgwQB3q6TWIbKcR1lPjw3Ox8tT/Io11x9AEtOzkg== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-widget" "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-widget" "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-cloud-services@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-cloud-services/-/ckeditor5-cloud-services-47.4.0.tgz#d3978b92528fe4600f8bacc5e8239a5ac7c4fbe5" - integrity sha512-6xUiyoMkcW8F/8OJrEGeKrMixRGLeQYHxij7tYyrXUqugdCJmZ5WNfvsoyVBwk7g3XQDSKnfKG28gSVBPirwBQ== +"@ckeditor/ckeditor5-cloud-services@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-cloud-services/-/ckeditor5-cloud-services-47.5.0.tgz#36810ef954fb08e5c0e8cc36c6e8462ce27b0ee1" + integrity sha512-lsvOK5w99+GfwQz9UyXaKEbH70+DEy0+wvO+82lgd5vpOiqMYfHz18b1abi5ICCTMo+m3KZyFFj33NoelRWq6w== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-code-block@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-code-block/-/ckeditor5-code-block-47.4.0.tgz#0898ebb555689eda97c50345eb2094e1e208dc9b" - integrity sha512-lfZd1Zu6FvHbOEXa1yJnuRDK0jYXZR0OaV9ek6A2ZQ6Z169Brc+aH1sTakw7r6S8m1clTz+vRH3UuVk7ETsQGA== +"@ckeditor/ckeditor5-code-block@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-code-block/-/ckeditor5-code-block-47.5.0.tgz#564ca4e1bbf571e72256a89c726ca5729064cc34" + integrity sha512-DdfmlvIu4nRgX41bUTkGlkL0PGJEIKcL3vVRniLXjgqnNrAsR5kWdbYTOu+qSvlCVzr4Z11nAG49QZIJ/aeAGg== dependencies: - "@ckeditor/ckeditor5-clipboard" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-enter" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-clipboard" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-enter" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-core@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-core/-/ckeditor5-core-47.4.0.tgz#16413d2fe25e456ddd97b67d3e2b4bdf1ff1b4c1" - integrity sha512-upV/3x9fhgFWxVVtwR47zCOAvZKgP8a8N7UQOFwfs3Tr52+oE1gULWKTiS9079MBaXaIqtM/EbelNdvBh4gOGg== +"@ckeditor/ckeditor5-core@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-core/-/ckeditor5-core-47.5.0.tgz#5919f424fa538437914418cdc4311342ca36cc11" + integrity sha512-4LdO9HIJ/ygN+YC4pty5plmFqlvHmjuQx1zdzzmbL3VSR4MCsaongDX4vVqpUcRVLzxM/1kErP0Da6cpVbkqzw== dependencies: - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-watchdog" "47.4.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-watchdog" "47.5.0" es-toolkit "1.39.5" "@ckeditor/ckeditor5-dev-translations@^43.0.1", "@ckeditor/ckeditor5-dev-translations@^43.1.0": @@ -1033,316 +1034,318 @@ terser-webpack-plugin "^4.2.3" through2 "^3.0.1" -"@ckeditor/ckeditor5-easy-image@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-easy-image/-/ckeditor5-easy-image-47.4.0.tgz#fcbb1d470e1e4e80c0198a92ca8d6bc077b55dcf" - integrity sha512-YMxvD3Gh6kVux1OKdtdubvjtUHu4TIN7YgCThqsfnuumpnx94Dhq3+wy8o/dO73dRcq/iVvb/9LmkivT4+8uXg== +"@ckeditor/ckeditor5-easy-image@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-easy-image/-/ckeditor5-easy-image-47.5.0.tgz#c47e54bb58b40649f0986f886faad5a2958f9502" + integrity sha512-e71gKQyA0UnSun4XLXawg5SP+IV2N/jhw8mu4FoFcRxhqV51FAwonldzwfdNgfD9lpX6bhYi3gREPGnuwdVPgA== dependencies: - "@ckeditor/ckeditor5-cloud-services" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-upload" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-cloud-services" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-upload" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-editor-balloon@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-balloon/-/ckeditor5-editor-balloon-47.4.0.tgz#489dd513869036b673d5d9f4d7408dd39693bb58" - integrity sha512-FZuHy5EhzssTQZTuXQF7aVRJyvY0QaIOr6yj8fttRoWQgIDMzJNm+rVW9C9FRa1+j1i9tlrE21+GYIhCgEGyOg== +"@ckeditor/ckeditor5-editor-balloon@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-balloon/-/ckeditor5-editor-balloon-47.5.0.tgz#229cfb65c61069a00297fd8857f22333e91876c4" + integrity sha512-UrMEb65RrEmV/v/DvPpGIfFVa8KCoVnONALyDRPQ8zsdp745vKuMi+i2jHGGdhKWz6SYDfzJROQ10dq3FMxuuQ== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-editor-classic@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-classic/-/ckeditor5-editor-classic-47.4.0.tgz#c41243dfe3e2029d432db43c537ff73775c2f483" - integrity sha512-b698aEHRJSC4jMP0fYD78tdqMw5oQHtCpUL6lU8LFsysCe5M0cqgab4V0hEjeIsg4Ft/UmkgFd1aAleRCDftJg== +"@ckeditor/ckeditor5-editor-classic@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-classic/-/ckeditor5-editor-classic-47.5.0.tgz#2b74e7698a8b8ed0ac432c7bf8ef230ac830fd58" + integrity sha512-OPu3OiaXm03r+mGhzQMdRyMFnJYMT9VPC8+e91e52GVQh/8sR03rwoow2RbNvsznP/+fAVnH4LrIgRR8K5cgUg== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-editor-decoupled@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-decoupled/-/ckeditor5-editor-decoupled-47.4.0.tgz#df4bcd16c03addcedb2c5b56887850eba592558c" - integrity sha512-4Nk/fe5Sob9aUf8gf4K7GQjqI0XftDThGRjX1eKOSDs+OGXRyB4Fxtu+tHLCyCt8cITac/PAMWaO7dwqbAK8bA== +"@ckeditor/ckeditor5-editor-decoupled@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-decoupled/-/ckeditor5-editor-decoupled-47.5.0.tgz#b6503cea5ed12485ddf606091e113521957b5655" + integrity sha512-bO8+DysIcgdrqo26InvoZ6geyoCBExs/V6c3DPhbG/pUuMXc9F7Zfn/hmKo1iOWL8E+gEJosu7psP/PiYM8RAw== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-editor-inline@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-inline/-/ckeditor5-editor-inline-47.4.0.tgz#05576fe4bd2a6c41ab4845ca4584981c52ae08bf" - integrity sha512-/xKtAwq0Pg3Zq7q9QcmrUnqc8XScrUlixWnl58gOxsdmflaSaK4qLtnId0FmSrax0tqVp1qihsUfvE5uUNnyGg== +"@ckeditor/ckeditor5-editor-inline@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-inline/-/ckeditor5-editor-inline-47.5.0.tgz#28a083c28b86e6cde1696a3d0fa1b074b4e378a1" + integrity sha512-L+YSTq7Hlt9nmip9xwPBAW1rNQlS8WGIfgXAgOhcdXsUE7r43PA5gjyckvRB0vKASOF5+c6PTDjiLpapNVnv4g== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-editor-multi-root@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-multi-root/-/ckeditor5-editor-multi-root-47.4.0.tgz#bb41b4c5a076c23f8dcb51660ecfbef300db84b5" - integrity sha512-gKYQeg2QI+9JM2gujYVBaLVlh7Dw4XfkX1g4jYMEqq4YG5E17Hpbc1A/IqUb0LLpAd1TG64AR4s/vxK0JrnY1g== +"@ckeditor/ckeditor5-editor-multi-root@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-editor-multi-root/-/ckeditor5-editor-multi-root-47.5.0.tgz#f5900fc795a2319ba8e9787cf66aae052339be01" + integrity sha512-tpI5VMwT+O3dXktORZ1Z4eCDp9sFEsxxEDDv/mmGz/6s2h+9rjEIfi8KqVnRfEC+uW/H2pu+q6r5y6/S3c+unA== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-emoji@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-emoji/-/ckeditor5-emoji-47.4.0.tgz#b3214f8668f6e8c1621153f3535154015d356030" - integrity sha512-PbTqvbBzMfvKaxTzAt72VskT8ifGoKRNKzskEmm74RCLu6a60rUaqL/4ChkTsF1FKPvB07VDbyDQx4XkvUOBIA== +"@ckeditor/ckeditor5-emoji@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-emoji/-/ckeditor5-emoji-47.5.0.tgz#31a2ce0fe2013052d8fbf80d86b3a566036ed73c" + integrity sha512-NVSg4hMpzyq0eVmVugBmY//McJpiyK0/XsXx72qySSh1b4FfKVT1couNB0m1pM5sFm7hXYB33WXyTpB4imyKyA== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-mention" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-mention" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" fuzzysort "3.1.0" -"@ckeditor/ckeditor5-engine@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-engine/-/ckeditor5-engine-47.4.0.tgz#7711c1e3bfc7912f56ba22310d8299691e18b02c" - integrity sha512-U3Zq3qZ86Si6L4BslJIXotK9oVXu59zAuDVWlx3prAUS5Mrz7MfVlWdz9HeWu9W1i2FmUGVksX+uoO/ng2CZUA== +"@ckeditor/ckeditor5-engine@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-engine/-/ckeditor5-engine-47.5.0.tgz#c9bc555284dd691aea17a3f4a0eb72d88638ed8f" + integrity sha512-4T6MzgjV5C7em5VgaHJ+RuN4gipryTErqtjtmb/RJXvfrEg37CYOpdHurM/VwY8hiD7yGUADB2iJGroLtytzCQ== dependencies: - "@ckeditor/ckeditor5-utils" "47.4.0" + "@ckeditor/ckeditor5-utils" "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-enter@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-enter/-/ckeditor5-enter-47.4.0.tgz#9d6b5038a43c926cb0245f6bd19c52d80151e99a" - integrity sha512-BQjJ7CjXENoF8Inv8ydRl+luRMKQvw1ohkiYsTEruHjGKkAFyDTGrorzkoGp2IU98n5SVGJE+XwVxpKgjsKAVQ== +"@ckeditor/ckeditor5-enter@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-enter/-/ckeditor5-enter-47.5.0.tgz#5e7221ce635369ae2f6c94e280f12c42b560e4f8" + integrity sha512-knpo3bZwFURor6gnDN2oS2mAA9qoPvMsEjuS3hce5K6pQAKqyUHqGSIQax7dLBW1spaWExvI+OTi/UNtNyPLWA== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" -"@ckeditor/ckeditor5-essentials@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-essentials/-/ckeditor5-essentials-47.4.0.tgz#f0100ebe4ec1dedf427648848571d722d076faa8" - integrity sha512-M+8xGJF+PKEcTjTeqofNe6cjcTnsy6EomqwGrbHDHhyAXC4d8k/vRrptymjonW7H9IsuOcQ5t2eZj3d+yl03gg== +"@ckeditor/ckeditor5-essentials@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-essentials/-/ckeditor5-essentials-47.5.0.tgz#4622134a31ade31a1e45d402849e8660b1a41872" + integrity sha512-uEdq/jnWPuN2KL6RufpQW9smnJGnU2f5/a5/QuP+rIfzPmpCmwCjgQto+8/lpCzWvEjUoHUsuIHlIT4TFYUYEQ== dependencies: - "@ckeditor/ckeditor5-clipboard" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-enter" "47.4.0" - "@ckeditor/ckeditor5-select-all" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-undo" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-clipboard" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-enter" "47.5.0" + "@ckeditor/ckeditor5-select-all" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-undo" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-find-and-replace@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-find-and-replace/-/ckeditor5-find-and-replace-47.4.0.tgz#7ae9606af2def3a71f75883d6d69e126034e3911" - integrity sha512-CZAX1XxrJcnOAwENfw4x4DiLyZ6uOHUHJqFXyyJdQC9qfEizvFYTXn3zO6fbViyDd/k4ugAoLBjpaZh6p9FyOQ== +"@ckeditor/ckeditor5-find-and-replace@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-find-and-replace/-/ckeditor5-find-and-replace-47.5.0.tgz#df21b3c6afe49e5aac0e299aa8a73bbf66d9836d" + integrity sha512-30yr8zzztiVxVmYzKgp8snDWh4sJJc1FbS3UM+u9coNGdlxzpuy9SdseNRPOi+tTXsK1OI6/amWJltYmDsAseA== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-font@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-font/-/ckeditor5-font-47.4.0.tgz#c93d96c7c96a584f708d98380658e20e6781e7a9" - integrity sha512-QRIThyZg0kT1R4LTotD6cV9gm0NX3Z0Cq/IOQtuwTbRb3wa+kWXhVfKZPV9Qru5HifvrCrcWXMjkBRRIdfqp+Q== +"@ckeditor/ckeditor5-font@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-font/-/ckeditor5-font-47.5.0.tgz#6c04c37db3c319c8aad73e730040bd21d35b30b4" + integrity sha512-qh9GLJ9+9DsdPXgZg8dH8D5mv/ZZ0mK+Ulwct0rSxIBLruFK5EhjXjngIG8HUv30BGCIpXPawGDTPv2txQ4vlw== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-fullscreen@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-fullscreen/-/ckeditor5-fullscreen-47.4.0.tgz#a1eaae21a4f3de061bf86fa34c1ff37f18cc9491" - integrity sha512-DdroZD1cgNU3up74ZQq84vXyCDknQJJyyxQIXS5CKJy7qNR9YmixpVmyXpYJmZzdSVvp/p8Ej87VlOXfju3ilQ== +"@ckeditor/ckeditor5-fullscreen@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-fullscreen/-/ckeditor5-fullscreen-47.5.0.tgz#130a8ec9624d0a70c3fc67e74a41c6aafa6c168c" + integrity sha512-cdgpNSOKrqKZMslG9hnQQoWRUy+tAVi7r0oGIbDfMezvLnaJDoG7GmYSWbGe1/+l90/TMtxKwYM7cQ6dmqutFA== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-editor-classic" "47.4.0" - "@ckeditor/ckeditor5-editor-decoupled" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-editor-classic" "47.5.0" + "@ckeditor/ckeditor5-editor-decoupled" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-heading@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-heading/-/ckeditor5-heading-47.4.0.tgz#632bbc6eb40fcccce90fd2177e53e588cceadc0d" - integrity sha512-VWBxQ2ngrT0x50Tb1klZyIOykgNPby8sw5rBq/nv/UXBb2Ql/crp50miC8pBCOvkbTP16qzVbl5HoiltJQkH/g== +"@ckeditor/ckeditor5-heading@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-heading/-/ckeditor5-heading-47.5.0.tgz#56c097ea5f4316e34069fbc2c7c7f54fbdc7993e" + integrity sha512-TEHY+unz+1fnx3554TgoYNGw27Vct7aDm0qw0U5/+PkZIyAxLcElkzRqXu3HhAPy6qs6pqxpPfKWWtREJhKluQ== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-paragraph" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-paragraph" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-highlight@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-highlight/-/ckeditor5-highlight-47.4.0.tgz#3ff7a0314c72649d3754b578699f4ae88d538aba" - integrity sha512-SHBkoMVu/uTkvE0/1zaehlvCpEqYuh/u1Rh7SHNysrD05Nacs1t5jw+l2lTFoyJnhTy+RA9IONYSDF+5tK3dqQ== +"@ckeditor/ckeditor5-highlight@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-highlight/-/ckeditor5-highlight-47.5.0.tgz#387550fe2965c4308a6f390fabd78cea4e7e6b3f" + integrity sha512-ajLkP2jA51B+K83g/Ujyuvpe+Wst9iniPBASmMn03pkC3ZNkdDRtCmz1Kkhe0UpYIvhCIKeWHIaH+7BS0DlbAQ== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-horizontal-line@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-horizontal-line/-/ckeditor5-horizontal-line-47.4.0.tgz#02e27c27fa0f928ad41bafb9b8cfd961e5049396" - integrity sha512-UvL0x55QxRGiem8EPO9n/WQk6218TDNatKSCRueZkAYUrFC1bmtVs9g6GqvSl59RoRGcTxVcz0fXbsxrhZY6HA== +"@ckeditor/ckeditor5-horizontal-line@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-horizontal-line/-/ckeditor5-horizontal-line-47.5.0.tgz#726bfa24532d4c9f7b579d97cf36f70a61db0291" + integrity sha512-/lPpkiish+KLFCV+oSJ11XldS4MJdOogfKAtWkpdlzLS64/Duw0fxOUTk8lzo3tHAgZgP7Ji9FJM1HtiNehTNw== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-widget" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-widget" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-html-embed@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-html-embed/-/ckeditor5-html-embed-47.4.0.tgz#6cbcdd19341f18513c1c8349c68004bf53a7dd54" - integrity sha512-SnidyadvuC0ohT2kZ0crsnFy8adQwhHcRaGUNXx5qAHRK7K1wGp3nxdnyOW5GdK2CIe8DTo+H3v8nXfvt7VgnQ== +"@ckeditor/ckeditor5-html-embed@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-html-embed/-/ckeditor5-html-embed-47.5.0.tgz#10a35f39e4aa249f5e98fae21815fca9a628070f" + integrity sha512-7gDouRaElSoICODnCwmziNxKJnvApur/QPLt374q7gz2l3sCrSM7LuDuCDAXWTQGQGkA1291OrOKQtuXGwY/xQ== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-widget" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-widget" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-html-support@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-html-support/-/ckeditor5-html-support-47.4.0.tgz#69111ef5781ee732876beb77c40c1e347b2bc4d3" - integrity sha512-SGd6wvPB9VGNqEWvoEdK1kQJ3lpvrTNfsA5Pg02V/Zr3gIxnAqajYEArWDYtsz3ajaUDs06i1tFdpCbFB7JRMg== +"@ckeditor/ckeditor5-html-support@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-html-support/-/ckeditor5-html-support-47.5.0.tgz#e126a6674ef4bdc05038592d8e54c7d170299918" + integrity sha512-e/OuWdlh6EbE1ZrQDu0/QnMd+V1XdwTmUJkOQrwwZ0x8CSKvKOt+Nb40ac0ShjTE190dMD1tGfOjM0yDpp0neQ== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-enter" "47.4.0" - "@ckeditor/ckeditor5-heading" "47.4.0" - "@ckeditor/ckeditor5-image" "47.4.0" - "@ckeditor/ckeditor5-list" "47.4.0" - "@ckeditor/ckeditor5-remove-format" "47.4.0" - "@ckeditor/ckeditor5-table" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-widget" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-enter" "47.5.0" + "@ckeditor/ckeditor5-heading" "47.5.0" + "@ckeditor/ckeditor5-image" "47.5.0" + "@ckeditor/ckeditor5-list" "47.5.0" + "@ckeditor/ckeditor5-remove-format" "47.5.0" + "@ckeditor/ckeditor5-table" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-widget" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-icons@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-icons/-/ckeditor5-icons-47.4.0.tgz#73a1fbd70f14cb859ee71118978690489cdb2b9c" - integrity sha512-2THOymXou/dBR+Jk69+/DzE3lK3QVk8+9eSKdWQ4+kvYom9MXT9RwKJNe3BlvqUNxBymI8eVBjdaQjfv3AOT0Q== +"@ckeditor/ckeditor5-icons@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-icons/-/ckeditor5-icons-47.5.0.tgz#9aecb267dc60f366b4281bea16e6811f2f1966fa" + integrity sha512-1LKzMsMrKHOEtLc9rw2C6iV5Wx6YCjd8I4WxFKQnxtW+F7+e4H4+rWU7ILpUxYjubEx0FSUJsyyNB9B98GMUgg== -"@ckeditor/ckeditor5-image@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-image/-/ckeditor5-image-47.4.0.tgz#6222e3ae17fe6d94b609afabbd7e0d605e1ffcb0" - integrity sha512-Z0q+cANAvzvW/3lIMg0rpvVHx4nlWbUsfPw78gM7/DmB4qpdbKsX07iTut84ZnWvOP+WU3XIrhinMXTvl6IqEw== +"@ckeditor/ckeditor5-image@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-image/-/ckeditor5-image-47.5.0.tgz#8855bb8cd4542c9a59daf974f18fe2d457885833" + integrity sha512-Zm9Qvj5UUC+bKHtQNmPJisTS9Sy1z6UQ6pbH2OxKPcw7ckhozQIeRtZQqoSSKQWq9B9iJ+CPvE3b+7FYkOXkew== dependencies: - "@ckeditor/ckeditor5-clipboard" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-undo" "47.4.0" - "@ckeditor/ckeditor5-upload" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-widget" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-clipboard" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-undo" "47.5.0" + "@ckeditor/ckeditor5-upload" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-widget" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-indent@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-indent/-/ckeditor5-indent-47.4.0.tgz#ddbd56d04ab80c4a5bf2039197e778ca4e0487b1" - integrity sha512-lFPYPUSuByK6GHiTnkHeLkWHD5/SbXCQ5TJVzRJ3uaWvbqo0b0Hvoz92vtKueOwi1QsgXD38aYhMljs0h8eP5g== +"@ckeditor/ckeditor5-indent@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-indent/-/ckeditor5-indent-47.5.0.tgz#2c439c83d5fa6b40786519d08adab9079e919de9" + integrity sha512-ZioulqHdwXBqirZGwESdkXWiYa1AE+EVbhegMO+a8tNa+SCDIt4do5PsrWxyz+PtW5jGBTKY831TY4NIGTihvQ== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-heading" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-list" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-heading" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-list" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-language@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-language/-/ckeditor5-language-47.4.0.tgz#30ea15cde33cc28d7e1582bd7f44578becf67534" - integrity sha512-3FEoS59ZOTm6m0m0O5qEpsf4tGX/r+r0LjkDrRjhIcaGJh0W4Ao2J6cSrXv7hikDpgBjbHIkEy0V6KkIWWAZpg== +"@ckeditor/ckeditor5-language@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-language/-/ckeditor5-language-47.5.0.tgz#d718d54328b8ea9ca99d6e489b77ddd886b91ed0" + integrity sha512-BSSUxiqXNGEz9knYTgMJF6wpKjCY9NEsFBmfVzsXiIuNpYSXrS281Efl49E1ivjlnsgyEJLvmed6DOa+mkUHpQ== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-link@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-link/-/ckeditor5-link-47.4.0.tgz#f6aad2f7f03d2688db0171632c5f06c775725f80" - integrity sha512-AF7TVV64iOqia4x4psHakYYznPoS3I5j1Gijoa7jiTLGJZSaAL7xAc1qAajgWQ66o7DWuVGL7QkZwKIo1jlTPg== +"@ckeditor/ckeditor5-link@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-link/-/ckeditor5-link-47.5.0.tgz#1d62c5a3f010f42c630894d99cf14310b707604a" + integrity sha512-sxsB0dSEsJL1g+GTJ4dTQ33eulS4/mZ2wwU8TgN5EtpPjGR1DcsFM/Hbhpr+nC6pyGfYzjV5GnhgOT/aYGE2lg== dependencies: - "@ckeditor/ckeditor5-clipboard" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-image" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-widget" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-clipboard" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-image" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-widget" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-list@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-list/-/ckeditor5-list-47.4.0.tgz#daecd93432dd43a0d8eba9b58923c131a4fa8a46" - integrity sha512-OGvAgS+NB1dzrqhN1xEVfN8PTM73pjMnmDvQeQurwIfjQdJaO07jGPRqujQzNostckWvNPtQysXkbnp+QiCPOw== +"@ckeditor/ckeditor5-list@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-list/-/ckeditor5-list-47.5.0.tgz#07dcf8aab14a054d1885d173aa8ed8cb7f04a021" + integrity sha512-9Yq2Jsgebbdxe6As3jviwFXL4CH38Mb+YUjlO2oEZZEw6fDvwjc83ziBXA/t+6cyBTdLjibbFhGLyPeiy+Y0Lw== dependencies: - "@ckeditor/ckeditor5-clipboard" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-enter" "47.4.0" - "@ckeditor/ckeditor5-font" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-clipboard" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-enter" "47.5.0" + "@ckeditor/ckeditor5-font" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-markdown-gfm@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-markdown-gfm/-/ckeditor5-markdown-gfm-47.4.0.tgz#7ec446c002d000d53b0c5ee1a756add477718019" - integrity sha512-2W1dBzxPIdEsE0CiU19K4xQfBS2jSBruJh5XV924eyuJPh76CdXKDGPBwuVd6i1oK7x+ji0Griu9Y+R2F0jRIw== +"@ckeditor/ckeditor5-markdown-gfm@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-markdown-gfm/-/ckeditor5-markdown-gfm-47.5.0.tgz#6dd060188b2397a5b221dce26608f9ef6a04bb70" + integrity sha512-sxttfFji9jTHDKdDklN13w480tI+tLwATIOHO2xTDbsA18tp8opeBFNUUfEeQ8XoZkAxckwv5tFu9Au2XuAdJg== dependencies: - "@ckeditor/ckeditor5-clipboard" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" + "@ckeditor/ckeditor5-clipboard" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" "@types/hast" "3.0.4" - ckeditor5 "47.4.0" + ckeditor5 "47.5.0" hast-util-from-dom "5.0.1" hast-util-to-html "9.0.5" hast-util-to-mdast "10.1.2" @@ -1358,271 +1361,271 @@ unified "11.0.5" unist-util-visit "5.0.0" -"@ckeditor/ckeditor5-media-embed@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-media-embed/-/ckeditor5-media-embed-47.4.0.tgz#611ea3ccc49c4a529da966bd792cd210a3ef6515" - integrity sha512-oL/In6Q3dtgj23FyyKbtYa704sl1eEx8JeO4ODRL3scCNI2/7qx9nGMexydiJi+Saulvs/3g7A8PbXiI+iArog== +"@ckeditor/ckeditor5-media-embed@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-media-embed/-/ckeditor5-media-embed-47.5.0.tgz#0c40872aaad5d68853c5c59bf09c42a784165243" + integrity sha512-aA+MALRNzyeXcfXoEKDJiun0VU5p8llVrXibrXKBK4Kpx53TdBd75+OUe4z8wOcKYN50ntRf7YSGCLB0yqJuLw== dependencies: - "@ckeditor/ckeditor5-clipboard" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-undo" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-widget" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-clipboard" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-undo" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-widget" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-mention@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-mention/-/ckeditor5-mention-47.4.0.tgz#362e1e63898215f8df4c2abbcd908bb7408a6056" - integrity sha512-1niRMaI5HxYbSTosxjU/6F5Uo+2hCEa3s18emwIBMTG1zOu0OViubuj+P8wCOqmSmpzvfkNybl4kk74MahGk0w== +"@ckeditor/ckeditor5-mention@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-mention/-/ckeditor5-mention-47.5.0.tgz#278c4cb3851b0c74fdd6d4121a0b6e19923a8bfd" + integrity sha512-AhfdHcG6TNRLP/gEJIPdZM792vbjzQUR6lwXy7vFRIfVwDZ+qanEIKsTJue0x1oJYkdxC0hSRx2ZN+4QHwEOFw== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-minimap@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-minimap/-/ckeditor5-minimap-47.4.0.tgz#0aeffe10bc25f850bb57656d183c5c80faad3b42" - integrity sha512-j0bOrjhEB5U6wCrz8CgW8ueFgHJJORtgqkOiRfQd++SBHGULSRr/WJwvaObcrhhNrY4Mlme8Nws6s5YJxzlFhA== +"@ckeditor/ckeditor5-minimap@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-minimap/-/ckeditor5-minimap-47.5.0.tgz#ef05d0f2f9d6cf477b26bec3d820da9c400bf58b" + integrity sha512-GarVZ6e29UY6KWlKZcCQRgEggU1oqep3J6wiXJ0hxrcvYOsDS06Zo326xdcs9zQEC3M/siBO/4WW77gBZdjB0Q== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-page-break@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-page-break/-/ckeditor5-page-break-47.4.0.tgz#bf25609dd31c6e184570522ed54f60855056167e" - integrity sha512-v4VR4OhLqj5Rp/Dwb9BSb9lSNAkGVF9n5ThvC0dFeHMikC4ENcqH8NpcbVnaua4tsM9tX0jZLHbcX+jMune4IQ== +"@ckeditor/ckeditor5-page-break@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-page-break/-/ckeditor5-page-break-47.5.0.tgz#113c0834e610e833b82c7cf1beba7e931f5f4535" + integrity sha512-e7//l3ls4vVsRXcsX9Lg6r9EBjT5Aq6UfHB5KWpi3bH+S2tqE8CoeQZPKFF7fa6W2V1BVUolkUeN72iiOkCi6w== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-widget" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-widget" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-paragraph@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-paragraph/-/ckeditor5-paragraph-47.4.0.tgz#dfd2f314bcb3b0557a16e9c8ae7be5e5d5201a95" - integrity sha512-epw82iXcK6togOeE/rolQBkyxCfz8m30VoH0bdq0YKkg8+HJ5uzB2FweFDH+l/cyoubdB2f1370G2dAMp6huBg== +"@ckeditor/ckeditor5-paragraph@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-paragraph/-/ckeditor5-paragraph-47.5.0.tgz#7016e3440e532acf1fb60ac43a0efb50f438abf5" + integrity sha512-dEOJ876f8kvpkNBCOSj9g+2x6Ig9e41k0Gdp3WrkNcX+3QCthNeWa5XJfPDsQqTINldv+L9GBwxgFEMRi0Fq/g== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" -"@ckeditor/ckeditor5-paste-from-office@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-paste-from-office/-/ckeditor5-paste-from-office-47.4.0.tgz#b1b37e743ca71548e6b3e7d258be34ce4e42a9df" - integrity sha512-yKOk+CDV0dAy+XeqUcP5Drur1u69h6UCdLwDUEbS/egSv/+o+tJwCGrTCRzPqBeUxIahUGBMk0obID7v6xT9IQ== +"@ckeditor/ckeditor5-paste-from-office@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-paste-from-office/-/ckeditor5-paste-from-office-47.5.0.tgz#5f2ebb0d30ec317dff6d74610f93bf7dfdca3a89" + integrity sha512-xF9TXExWPPVgIEA1gxf+oFNMVSOsbpGgRSNKS23gN5scxLupNGN/G1hj0UGTgHaYczBiOqW44bu82Lgduz7/GQ== dependencies: - "@ckeditor/ckeditor5-clipboard" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-clipboard" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-remove-format@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-remove-format/-/ckeditor5-remove-format-47.4.0.tgz#1461db9343d708a79c24c0af07c70e54a783d999" - integrity sha512-XD6LY76m3bZr/twRGTjNRnU4z0VU1akDC7evVMhRPaDruR71km00VT1YNPRChCDmdssEVeWEynHhLQ/kRjy+0w== +"@ckeditor/ckeditor5-remove-format@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-remove-format/-/ckeditor5-remove-format-47.5.0.tgz#530d52bd163a9a46c9643f505ab9da4bac2fca65" + integrity sha512-QTR9ye1iWfnGu7h5a4RONEf2e4y21dC847opNUozO06g51/e7G6iXybEcRtBqs51gpVddKelUbxUV2Zp7tm8iw== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-restricted-editing@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-restricted-editing/-/ckeditor5-restricted-editing-47.4.0.tgz#29df0b5763d973d5e1713fea194abe6e72d32c85" - integrity sha512-roywT2jKCs0NVd6TVhYlmrnP0oI4499M5L1mV8Vqq4wc9puVeEPSIKoZNdIF5YWXsHjpCUCMejpuigLTIbf9MQ== +"@ckeditor/ckeditor5-restricted-editing@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-restricted-editing/-/ckeditor5-restricted-editing-47.5.0.tgz#6511a83f91bbbed157242252f829aec6023b548b" + integrity sha512-hL6pr3L1xaERwGBNG37AcLwj5XVXqAzYUWKF/pDbT8FnHymA7jn1dbsUHHbmPaqXtk8zu7W9k+aGKXwrkPUFVg== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-select-all@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-select-all/-/ckeditor5-select-all-47.4.0.tgz#6759a9474ecac0df1d75b2361b0521a7f5e8e7eb" - integrity sha512-9fVsmNFmSj53kJKPKUmCkgpXUev2OeMJ5cFVKXvzEvsm6jFTO8/9iHRTbN/j/ZzWuK5MoO/I3gVn4wGOIX//zw== +"@ckeditor/ckeditor5-select-all@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-select-all/-/ckeditor5-select-all-47.5.0.tgz#5fce145bb23e1565aa039cf4604b5ab8e8d75d16" + integrity sha512-g+bLHv5aqgtuGhqLo9FNYS4aSGZztb15myydHXvUBJ7OgY3YlVDILi0xd9WrySPpO+Axi0ATdl9evO42rPYVJg== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" -"@ckeditor/ckeditor5-show-blocks@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-show-blocks/-/ckeditor5-show-blocks-47.4.0.tgz#0ae9ebbc8f13a48f54cb969dfa09145fc552ecee" - integrity sha512-uIFHsH2HMPYRWmK+heZoiXRVqbxFJZwYZY1WmNKjE5g7OM8y+PVowe0ZYICjauV2/Z2rwCWtodDKb1bnVnl+mQ== +"@ckeditor/ckeditor5-show-blocks@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-show-blocks/-/ckeditor5-show-blocks-47.5.0.tgz#124c93f6e066e9daf0d5883bf05d733909b076cd" + integrity sha512-G902a+fvVv9IMcLm634yaehf7VYwWrSZqRQtD+rpV2wDaHcps5aXCHSPNrk5JdbInheF1d+V3uiySzbkc823IA== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-source-editing@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-source-editing/-/ckeditor5-source-editing-47.4.0.tgz#59635fd5d85988f84885bc455da8839bd79ca26d" - integrity sha512-AtamOK+Dya6abkuo9XYME05FYFigBRic5gr3/KzhyFfHh7qiFlZFLCDH0S/JEQ0AduFjfgUx4h0ST22RIhiYoA== +"@ckeditor/ckeditor5-source-editing@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-source-editing/-/ckeditor5-source-editing-47.5.0.tgz#7716a5c492a23cbb745502a8b1047e533e2dab73" + integrity sha512-YmTaRSVkbx441xa9foOz/z+cYqJ385yKC6cW+MACsDVdHj2ruvj1QUDrRpvPc8T0irTSAuhxQhhmCSIEnt61WQ== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-theme-lark" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-theme-lark" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-special-characters@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-special-characters/-/ckeditor5-special-characters-47.4.0.tgz#e220194e45a2cf0563cc30d2e81ed87a3a2853d6" - integrity sha512-eYP23WZY8ayA0q8LNVCUcP85yf9J2gSpVE9E6LNIku4rbzox6mCf0sZF0ZhzvqHyXyj9Mn+S21IZpLOTuTUW0g== +"@ckeditor/ckeditor5-special-characters@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-special-characters/-/ckeditor5-special-characters-47.5.0.tgz#2739d4f930447d2c7388f51324a1a6b562cf52a5" + integrity sha512-Wou/dBJnv3Qiuz7io8YTL5LJGS3JqiGwpw6nLKPxADinZfI8Rr+ZOvw5BL8ifYkPudOwLFKFwMY3/4I7tZQAhA== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" -"@ckeditor/ckeditor5-style@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-style/-/ckeditor5-style-47.4.0.tgz#e6077f4875309733e7d56bccd02f284b57ae08c3" - integrity sha512-R6kt9jX9FOnYRXKn7kX0ZdIdW5A3S7ZZBfcdwzG9O/t7r5IIkp+yhC1y6/uBAc2twvvqMhG7Gu5KH2o/TVVjSg== +"@ckeditor/ckeditor5-style@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-style/-/ckeditor5-style-47.5.0.tgz#46763990d05acaca5a52b607024379727bcf102b" + integrity sha512-JmXOZQvRoOeNotZyXdColDncBw9GIl6nFbL8lVHZL/BF6wFxRHlJSjy7W/VdYAsHv3GaOKlKWMRg72k+17mgUg== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-html-support" "47.4.0" - "@ckeditor/ckeditor5-list" "47.4.0" - "@ckeditor/ckeditor5-table" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-html-support" "47.5.0" + "@ckeditor/ckeditor5-list" "47.5.0" + "@ckeditor/ckeditor5-table" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-table@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-table/-/ckeditor5-table-47.4.0.tgz#079fca2e5d4739966b59c6b5e0d6df41c9d97e25" - integrity sha512-gWraeB14YnpR+ELySu3xgSFlfur07ZBPN76rQuiIobrecKwhh1Az8rk7Qo4c1K/q/f4pHmqh87nhSprn7Mo7+w== +"@ckeditor/ckeditor5-table@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-table/-/ckeditor5-table-47.5.0.tgz#b97f6eb3715144229b496b0663f7c7089b8001c0" + integrity sha512-aB4Sn9+DLuDHd5pn5d/QdSGQYzGBLJg2+zlzSddcxSeUS0gj7qtOhtce2ChY899D1pszbdQHXbM/Lnwgy1ZrCQ== dependencies: - "@ckeditor/ckeditor5-clipboard" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-widget" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-clipboard" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-widget" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-theme-lark@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-theme-lark/-/ckeditor5-theme-lark-47.4.0.tgz#1731c864b4fc46d7440b7c5bc6e136527f1c1da2" - integrity sha512-kdtwV5HJ+8/oNcsGM8sdpULhXr2TfM7gEKlH/EAdycLDa6topcJuTl7iVSEu4hZzwVo2agiEMmdUIf3dvWweow== +"@ckeditor/ckeditor5-theme-lark@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-theme-lark/-/ckeditor5-theme-lark-47.5.0.tgz#8f34b0fe81cc845290d698f49ce40f938c20b144" + integrity sha512-vmQJyT6UnsmTF9rIWhYWWRGGpDhoxo9vd7aJ+m3b+gJqCLMziGadP6Dz3dezDQpbaW41Fk5CF61QPnMqaHuk7g== dependencies: - "@ckeditor/ckeditor5-ui" "47.4.0" + "@ckeditor/ckeditor5-ui" "47.5.0" -"@ckeditor/ckeditor5-typing@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-typing/-/ckeditor5-typing-47.4.0.tgz#4135bb117f0e7c38d63d1ee7c98e02283ff00d5d" - integrity sha512-+YmCUTLVAryK5h68TgQ0qxDngs1MTCLKPDXxHzNqs0oXHai9YkJv/zg4zeb0/RQRIps7jh3bPapZoi2hP2iN3A== +"@ckeditor/ckeditor5-typing@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-typing/-/ckeditor5-typing-47.5.0.tgz#6fee4243f88e869a7d561dc1b9b41b8b3429f03f" + integrity sha512-/MoO59Cl/XJ4f79ecc5BHBb0sEnZttMosXpm1wjhwRKE7PyEICt2t+Uqi0zp9txl21O2z+Gmjz716X5InMa/4w== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-ui@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ui/-/ckeditor5-ui-47.4.0.tgz#3a2c653dbf890abd04dfb42edeb5ef833e30a541" - integrity sha512-sL67wp2DX+P3zxeJLo2I7yLhBlX6Zhd0xfUAB6vX6SkjhMeC0L2gLOIr3kKq/OMKEuS+0iZ+qVvEN1j+2Flzlg== +"@ckeditor/ckeditor5-ui@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-ui/-/ckeditor5-ui-47.5.0.tgz#5efa9d41038e270529a93f8cb465af729fe1465b" + integrity sha512-wn55rqImjQ44CPavijdc9+MGHZZJ9lS9++NEAmt95b45ywd9nAyJ4QcuXDZ6f+xjwfeTuLX7jhBSuz7w8FWcwA== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-editor-multi-root" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-editor-multi-root" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" "@types/color-convert" "2.0.4" color-convert "3.1.0" color-parse "2.0.2" es-toolkit "1.39.5" vanilla-colorful "0.7.2" -"@ckeditor/ckeditor5-undo@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-undo/-/ckeditor5-undo-47.4.0.tgz#8d92e91efb8929206a83374119a6692490fee036" - integrity sha512-OnxpJb9glDwuSTl59Yb4+6bjWW5h4BA+94YhesKZXeIaXjyzwFmNGxM07nRyaX4KXwGVP5y5JZC2xv5bCOXKSQ== +"@ckeditor/ckeditor5-undo@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-undo/-/ckeditor5-undo-47.5.0.tgz#8a9dbbb74b62e17895c832add712fa78bccd27bd" + integrity sha512-StiwvLrAlRqog1BSR8YZ5S6pdUdFB5BCPWPPJEAcMiBfRmG4pSzs+k3LIMgnyVoGwGBUcjdLVayqaESxe89Oeg== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" -"@ckeditor/ckeditor5-upload@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-upload/-/ckeditor5-upload-47.4.0.tgz#b0b6c232945ffea4f21d136b499393b837e566e5" - integrity sha512-9gMfYltVNi5aYNs8IixTXww9kyU0+oEeY9pN8W6YLrhToVJdnN14pW3yNkQJKJPK7HS2RgM6L1Y+u50qu/IL2g== +"@ckeditor/ckeditor5-upload@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-upload/-/ckeditor5-upload-47.5.0.tgz#6348083b0b301a64cc9cc49b9f2e8bdb8088aa1d" + integrity sha512-F1DPVYUGsb9P1/35vqPk7E+Y7wlubyb51DDR9irjUCAFyqQzolVQJ2bcsvrUXx+P8LT63sLCzH2do0pwMacGyQ== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" -"@ckeditor/ckeditor5-utils@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-utils/-/ckeditor5-utils-47.4.0.tgz#27477eed00a0b6059c29727783a77f26d2cecad9" - integrity sha512-+5v1k3+8Yr0VUnO+3GfP7MsDCqt5KD9f9Z5wUVRig/J61hPTv8cUQp0859K87IuOLdAP/rZ1iQpdi1psanQeIQ== +"@ckeditor/ckeditor5-utils@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-utils/-/ckeditor5-utils-47.5.0.tgz#a590672797cf2dcd7f62c70309c1206260894634" + integrity sha512-gkjlbVLjqkmZXo1lacY0kHRi9FTZjfJfL/OkF6WGauh0VLoDAIJHIv73PjqH6Sh7nF0Dx2p78NJ42W0t/KWxug== dependencies: - "@ckeditor/ckeditor5-ui" "47.4.0" + "@ckeditor/ckeditor5-ui" "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-watchdog@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-47.4.0.tgz#8ce4b3836032cf4635960dd6162e6bba46b5597c" - integrity sha512-MEfHIVYV4SILXi++G00y3wREm/1gT5dO+pTGpQY+NNYw8wgi32rg1q8hO2P/upsVaPzbeD3WLURyqeIxKwY20Q== +"@ckeditor/ckeditor5-watchdog@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-47.5.0.tgz#1b6ad7b68c2327cc936e38971902256846a20879" + integrity sha512-nmSnCvCvf9ChNS2C5YIdszSMeoGQRvd+CD4cDtLmkmaBqSuV/kePr5nGvqT8lMt5MrMtdqG/KmgE/xxmE4QIHg== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-editor-multi-root" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-editor-multi-root" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-widget@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-widget/-/ckeditor5-widget-47.4.0.tgz#2d63a8faa2df59f1c12d0c31b772a097e1bda048" - integrity sha512-wffwrMQ6h+Hdu9IMG0H0QAf0YWWn+AGeJwPs69cRjRwB5pNOCUmMyM4h8MtNp15UEvGGARlhOjFf1TniMUkKrw== +"@ckeditor/ckeditor5-widget@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-widget/-/ckeditor5-widget-47.5.0.tgz#a5c71a0f1c0f6284d161553c067576587361060c" + integrity sha512-o8wFSP5Dx2kR6t2XDzpdw1IZoqOrCXdJ51iZvClsUn6zQ+EosZonlmej8oA96ZsRK/nw9GOSt2cM0Snzv3zDtQ== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-enter" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-enter" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" es-toolkit "1.39.5" -"@ckeditor/ckeditor5-word-count@47.4.0": - version "47.4.0" - resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-word-count/-/ckeditor5-word-count-47.4.0.tgz#4fb9a5a23c347bbf56c9baccab6951d7e3d1b95c" - integrity sha512-JeiwHJyBdlUCdzfW3K2KoGO/QhDe1qOKNPXiVXzExIyZpww+hm5HjV/zi5gX4xAvWg9ew0UaQRco5Dy7mBBfRQ== +"@ckeditor/ckeditor5-word-count@47.5.0": + version "47.5.0" + resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-word-count/-/ckeditor5-word-count-47.5.0.tgz#839000c7f28a0b116f47394dc65e7784be1a4eb7" + integrity sha512-zsATU4eI7iFWC+3axpj9JG7Gm6IRzDzjW8fQqYho0xMc3hN71XxgIbPZR7jLpwNo29WirCsNxAfqi4Q2Ws8c6w== dependencies: - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - ckeditor5 "47.4.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + ckeditor5 "47.5.0" es-toolkit "1.39.5" "@csstools/selector-resolve-nested@^3.1.0": @@ -1830,9 +1833,9 @@ tslib "^2.8.0" "@fortawesome/fontawesome-free@^7.0.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz#8eb76278515341720aa74485266f8be121089529" - integrity sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA== + version "7.2.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz#188c1053ce422ad1f934d7df242a973fcb89636d" + integrity sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg== "@gar/promisify@^1.0.1": version "1.1.3" @@ -1854,17 +1857,10 @@ resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.23.tgz#a6eebc9ab4a5faadae265a4cbec8cfcb5731e77c" integrity sha512-GZ7cijxEZ6Ig71u7rD6LHaRv/wcE/hNsc+nEfiWOkLNqUgLOwo5MNGWOy5ZV9ZUDSiQx1no7YxjTNnT4O6//cQ== -"@isaacs/balanced-match@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" - integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== - -"@isaacs/brace-expansion@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz#0ef5a92d91f2fff2a37646ce54da9e5f599f6eff" - integrity sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ== - dependencies: - "@isaacs/balanced-match" "^4.0.1" +"@isaacs/cliui@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-9.0.0.tgz#4d0a3f127058043bf2e7ee169eaf30ed901302f3" + integrity sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg== "@jbtronics/bs-treeview@^1.0.1": version "1.0.6" @@ -2169,9 +2165,9 @@ integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== "@types/node@*": - version "25.2.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.2.1.tgz#378021f9e765bb65ba36de16f3c3a8622c1fa03d" - integrity sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg== + version "25.2.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.2.3.tgz#9c18245be768bdb4ce631566c7da303a5c99a7f8" + integrity sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ== dependencies: undici-types "~7.16.0" @@ -2453,9 +2449,9 @@ ajv@^6.12.5: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.9.0: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" - integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + version "8.18.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.18.0.tgz#8864186b6738d003eb3a933172bb3833e10cefbc" + integrity sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A== dependencies: fast-deep-equal "^3.1.3" fast-uri "^3.0.1" @@ -2609,6 +2605,13 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +balanced-match@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.2.tgz#241591ea634702bef9c482696f2469406e16d233" + integrity sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg== + dependencies: + jackspeak "^4.2.3" + barcode-detector@^3.0.0, barcode-detector@^3.0.5: version "3.0.8" resolved "https://registry.yarnpkg.com/barcode-detector/-/barcode-detector-3.0.8.tgz#09a3363cb24699d1d6389a291383113c44420324" @@ -2674,6 +2677,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.2.tgz#b6c16d0791087af6c2bc463f52a8142046c06b6f" + integrity sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw== + dependencies: + balanced-match "^4.0.2" + braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -2790,9 +2800,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001759: - version "1.0.30001769" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz#1ad91594fad7dc233777c2781879ab5409f7d9c2" - integrity sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg== + version "1.0.30001770" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz#4dc47d3b263a50fbb243448034921e0a88591a84" + integrity sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw== ccount@^2.0.0: version "2.0.1" @@ -2869,72 +2879,72 @@ ci-info@^4.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" integrity sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg== -ckeditor5@47.4.0, ckeditor5@^47.0.0: - version "47.4.0" - resolved "https://registry.yarnpkg.com/ckeditor5/-/ckeditor5-47.4.0.tgz#eb7879a23e780c356a2a48d477663d857494721a" - integrity sha512-6RTRV2w6nhmBSLBnA0O9QzcBC/Cf74ogziaKHOK61H+PcM6aP3ltb/fNScGyy3NVw3+OzaxjbPF7NSykVmmMMw== +ckeditor5@47.5.0, ckeditor5@^47.0.0: + version "47.5.0" + resolved "https://registry.yarnpkg.com/ckeditor5/-/ckeditor5-47.5.0.tgz#f40b2eef70fe768f9e23438bc6d32ac7362199b2" + integrity sha512-Ow4eLxOxXoaIeuugBh4fCidLuu3zICInWO6ugB0+nZRGn9dfz9ENfdk6UlShyV2B0C5Ocu2n6Fm6I5hlByC/qA== dependencies: - "@ckeditor/ckeditor5-adapter-ckfinder" "47.4.0" - "@ckeditor/ckeditor5-alignment" "47.4.0" - "@ckeditor/ckeditor5-autoformat" "47.4.0" - "@ckeditor/ckeditor5-autosave" "47.4.0" - "@ckeditor/ckeditor5-basic-styles" "47.4.0" - "@ckeditor/ckeditor5-block-quote" "47.4.0" - "@ckeditor/ckeditor5-bookmark" "47.4.0" - "@ckeditor/ckeditor5-ckbox" "47.4.0" - "@ckeditor/ckeditor5-ckfinder" "47.4.0" - "@ckeditor/ckeditor5-clipboard" "47.4.0" - "@ckeditor/ckeditor5-cloud-services" "47.4.0" - "@ckeditor/ckeditor5-code-block" "47.4.0" - "@ckeditor/ckeditor5-core" "47.4.0" - "@ckeditor/ckeditor5-easy-image" "47.4.0" - "@ckeditor/ckeditor5-editor-balloon" "47.4.0" - "@ckeditor/ckeditor5-editor-classic" "47.4.0" - "@ckeditor/ckeditor5-editor-decoupled" "47.4.0" - "@ckeditor/ckeditor5-editor-inline" "47.4.0" - "@ckeditor/ckeditor5-editor-multi-root" "47.4.0" - "@ckeditor/ckeditor5-emoji" "47.4.0" - "@ckeditor/ckeditor5-engine" "47.4.0" - "@ckeditor/ckeditor5-enter" "47.4.0" - "@ckeditor/ckeditor5-essentials" "47.4.0" - "@ckeditor/ckeditor5-find-and-replace" "47.4.0" - "@ckeditor/ckeditor5-font" "47.4.0" - "@ckeditor/ckeditor5-fullscreen" "47.4.0" - "@ckeditor/ckeditor5-heading" "47.4.0" - "@ckeditor/ckeditor5-highlight" "47.4.0" - "@ckeditor/ckeditor5-horizontal-line" "47.4.0" - "@ckeditor/ckeditor5-html-embed" "47.4.0" - "@ckeditor/ckeditor5-html-support" "47.4.0" - "@ckeditor/ckeditor5-icons" "47.4.0" - "@ckeditor/ckeditor5-image" "47.4.0" - "@ckeditor/ckeditor5-indent" "47.4.0" - "@ckeditor/ckeditor5-language" "47.4.0" - "@ckeditor/ckeditor5-link" "47.4.0" - "@ckeditor/ckeditor5-list" "47.4.0" - "@ckeditor/ckeditor5-markdown-gfm" "47.4.0" - "@ckeditor/ckeditor5-media-embed" "47.4.0" - "@ckeditor/ckeditor5-mention" "47.4.0" - "@ckeditor/ckeditor5-minimap" "47.4.0" - "@ckeditor/ckeditor5-page-break" "47.4.0" - "@ckeditor/ckeditor5-paragraph" "47.4.0" - "@ckeditor/ckeditor5-paste-from-office" "47.4.0" - "@ckeditor/ckeditor5-remove-format" "47.4.0" - "@ckeditor/ckeditor5-restricted-editing" "47.4.0" - "@ckeditor/ckeditor5-select-all" "47.4.0" - "@ckeditor/ckeditor5-show-blocks" "47.4.0" - "@ckeditor/ckeditor5-source-editing" "47.4.0" - "@ckeditor/ckeditor5-special-characters" "47.4.0" - "@ckeditor/ckeditor5-style" "47.4.0" - "@ckeditor/ckeditor5-table" "47.4.0" - "@ckeditor/ckeditor5-theme-lark" "47.4.0" - "@ckeditor/ckeditor5-typing" "47.4.0" - "@ckeditor/ckeditor5-ui" "47.4.0" - "@ckeditor/ckeditor5-undo" "47.4.0" - "@ckeditor/ckeditor5-upload" "47.4.0" - "@ckeditor/ckeditor5-utils" "47.4.0" - "@ckeditor/ckeditor5-watchdog" "47.4.0" - "@ckeditor/ckeditor5-widget" "47.4.0" - "@ckeditor/ckeditor5-word-count" "47.4.0" + "@ckeditor/ckeditor5-adapter-ckfinder" "47.5.0" + "@ckeditor/ckeditor5-alignment" "47.5.0" + "@ckeditor/ckeditor5-autoformat" "47.5.0" + "@ckeditor/ckeditor5-autosave" "47.5.0" + "@ckeditor/ckeditor5-basic-styles" "47.5.0" + "@ckeditor/ckeditor5-block-quote" "47.5.0" + "@ckeditor/ckeditor5-bookmark" "47.5.0" + "@ckeditor/ckeditor5-ckbox" "47.5.0" + "@ckeditor/ckeditor5-ckfinder" "47.5.0" + "@ckeditor/ckeditor5-clipboard" "47.5.0" + "@ckeditor/ckeditor5-cloud-services" "47.5.0" + "@ckeditor/ckeditor5-code-block" "47.5.0" + "@ckeditor/ckeditor5-core" "47.5.0" + "@ckeditor/ckeditor5-easy-image" "47.5.0" + "@ckeditor/ckeditor5-editor-balloon" "47.5.0" + "@ckeditor/ckeditor5-editor-classic" "47.5.0" + "@ckeditor/ckeditor5-editor-decoupled" "47.5.0" + "@ckeditor/ckeditor5-editor-inline" "47.5.0" + "@ckeditor/ckeditor5-editor-multi-root" "47.5.0" + "@ckeditor/ckeditor5-emoji" "47.5.0" + "@ckeditor/ckeditor5-engine" "47.5.0" + "@ckeditor/ckeditor5-enter" "47.5.0" + "@ckeditor/ckeditor5-essentials" "47.5.0" + "@ckeditor/ckeditor5-find-and-replace" "47.5.0" + "@ckeditor/ckeditor5-font" "47.5.0" + "@ckeditor/ckeditor5-fullscreen" "47.5.0" + "@ckeditor/ckeditor5-heading" "47.5.0" + "@ckeditor/ckeditor5-highlight" "47.5.0" + "@ckeditor/ckeditor5-horizontal-line" "47.5.0" + "@ckeditor/ckeditor5-html-embed" "47.5.0" + "@ckeditor/ckeditor5-html-support" "47.5.0" + "@ckeditor/ckeditor5-icons" "47.5.0" + "@ckeditor/ckeditor5-image" "47.5.0" + "@ckeditor/ckeditor5-indent" "47.5.0" + "@ckeditor/ckeditor5-language" "47.5.0" + "@ckeditor/ckeditor5-link" "47.5.0" + "@ckeditor/ckeditor5-list" "47.5.0" + "@ckeditor/ckeditor5-markdown-gfm" "47.5.0" + "@ckeditor/ckeditor5-media-embed" "47.5.0" + "@ckeditor/ckeditor5-mention" "47.5.0" + "@ckeditor/ckeditor5-minimap" "47.5.0" + "@ckeditor/ckeditor5-page-break" "47.5.0" + "@ckeditor/ckeditor5-paragraph" "47.5.0" + "@ckeditor/ckeditor5-paste-from-office" "47.5.0" + "@ckeditor/ckeditor5-remove-format" "47.5.0" + "@ckeditor/ckeditor5-restricted-editing" "47.5.0" + "@ckeditor/ckeditor5-select-all" "47.5.0" + "@ckeditor/ckeditor5-show-blocks" "47.5.0" + "@ckeditor/ckeditor5-source-editing" "47.5.0" + "@ckeditor/ckeditor5-special-characters" "47.5.0" + "@ckeditor/ckeditor5-style" "47.5.0" + "@ckeditor/ckeditor5-table" "47.5.0" + "@ckeditor/ckeditor5-theme-lark" "47.5.0" + "@ckeditor/ckeditor5-typing" "47.5.0" + "@ckeditor/ckeditor5-ui" "47.5.0" + "@ckeditor/ckeditor5-undo" "47.5.0" + "@ckeditor/ckeditor5-upload" "47.5.0" + "@ckeditor/ckeditor5-utils" "47.5.0" + "@ckeditor/ckeditor5-watchdog" "47.5.0" + "@ckeditor/ckeditor5-widget" "47.5.0" + "@ckeditor/ckeditor5-word-count" "47.5.0" clean-stack@^2.0.0: version "2.2.0" @@ -3421,18 +3431,18 @@ datatables.net-colreorder@2.1.2: jquery ">=1.7" datatables.net-fixedheader-bs5@^4.0.0: - version "4.0.5" - resolved "https://registry.yarnpkg.com/datatables.net-fixedheader-bs5/-/datatables.net-fixedheader-bs5-4.0.5.tgz#84f405e7351ac719022db6f97c5027b5bce5143a" - integrity sha512-R0m4Mntda7wfRCpyjGS2RWFw2861X8e4trn6SnBHID2htuMPPdk11bK4RVJMipgFDxdMfJbvEMH5Hkx5XKrNuA== + version "4.0.6" + resolved "https://registry.yarnpkg.com/datatables.net-fixedheader-bs5/-/datatables.net-fixedheader-bs5-4.0.6.tgz#25bc9d2d5f9ded665ea4915b26a433f74e3c2979" + integrity sha512-V5KhTssDq2osUG8aXur5wf8j6tXE9kSP/34C5k0DKIFkHjvZiK1yWPyadP6/T9JJRKWuJppPaLiJ1PzB+nlwPw== dependencies: datatables.net-bs5 "^2" - datatables.net-fixedheader "4.0.5" + datatables.net-fixedheader "4.0.6" jquery ">=1.7" -datatables.net-fixedheader@4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/datatables.net-fixedheader/-/datatables.net-fixedheader-4.0.5.tgz#075fff97f47efac9f3ba72d34f8f0ea67470f165" - integrity sha512-cobQhOhjzqIYXTvMRrHUulULS8Re+hd2mmgFiOGKcZwHV0mofIwBlgiU3Ol4LHikHUCvsGnTEXoI+C7Ozma5sA== +datatables.net-fixedheader@4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/datatables.net-fixedheader/-/datatables.net-fixedheader-4.0.6.tgz#0c361a8a90542d75402f897db401085433efcebe" + integrity sha512-icYg/qKDpqGDrAVRWfsjt0xQdngk48R7LWkS9t8kaZFp9c4xrLFcmmPtRLgPp5/S4JHZbbsxmVkF16kscjNZjg== dependencies: datatables.net "^2" jquery ">=1.7" @@ -4833,6 +4843,13 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== +jackspeak@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.2.3.tgz#27ef80f33b93412037c3bea4f8eddf80e1931483" + integrity sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg== + dependencies: + "@isaacs/cliui" "^9.0.0" + javascript-stringify@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3" @@ -5102,9 +5119,9 @@ marked-mangle@^1.0.1: integrity sha512-bRrqNcfU9v3iRECb7YPvA+/xKZMjHojd9R92YwHbFjdPQ+Wc7vozkbGKAv4U8AUl798mNUuY3DTBQkedsV3TeQ== marked@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/marked/-/marked-17.0.1.tgz#9db34197ac145e5929572ee49ef701e37ee9b2e6" - integrity sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg== + version "17.0.2" + resolved "https://registry.yarnpkg.com/marked/-/marked-17.0.2.tgz#a103f82bed9653dd1d74c15f74107c84ddbe749d" + integrity sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA== math-intrinsics@^1.1.0: version "1.1.0" @@ -5589,11 +5606,11 @@ mini-css-extract-plugin@^2.4.2, mini-css-extract-plugin@^2.6.0: tapable "^2.2.1" minimatch@*: - version "10.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.2.tgz#6c3f289f9de66d628fa3feb1842804396a43d81c" - integrity sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw== + version "10.2.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.0.tgz#e710473e66e3e1aaf376d0aa82438375cac86e9e" + integrity sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w== dependencies: - "@isaacs/brace-expansion" "^5.0.1" + brace-expansion "^5.0.2" minimatch@3.0.4: version "3.0.4" @@ -7455,9 +7472,9 @@ to-regex-range@^5.0.1: is-number "^7.0.0" tom-select@^2.1.0: - version "2.4.6" - resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.4.6.tgz#23acdfc09ee235eb752706d418c9c9ae6ccf67f0" - integrity sha512-Hhqi15AiTl0+FjaHVTXvUkF3t7x4W5LXUHxLYlzp7r8bcIgGJyz9M+3ZvrHdTRvEmV4EmNyJPbHJJnZOjr5Iig== + version "2.5.1" + resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.5.1.tgz#8c8d3f11e5c1780b5f26c9e90f4e650842ff9596" + integrity sha512-63D5/Qf6bb6kLSgksEuas/60oawDcuUHrD90jZofeOpF6bkQFYriKrvtpJBQQ4xIA5dUGcjhBbk/yrlfOQsy3g== dependencies: "@orchidjs/sifter" "^1.1.0" "@orchidjs/unicode-variants" "^1.1.2" @@ -7499,9 +7516,9 @@ tslib@^2.8.0: integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== type-fest@^5.2.0: - version "5.4.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.4.3.tgz#b4c7e028da129098911ee2162a0c30df8a1be904" - integrity sha512-AXSAQJu79WGc79/3e9/CR77I/KQgeY1AhNvcShIH4PTcGYyC4xv6H4R4AUOwkPS5799KlVDAu8zExeCrkGquiA== + version "5.4.4" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.4.4.tgz#577f165b5ecb44cfc686559cc54ca77f62aa374d" + integrity sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw== dependencies: tagged-tag "^1.0.0" @@ -7838,14 +7855,14 @@ webpack-sources@^2.0.1, webpack-sources@^2.2.0: source-map "^0.6.1" webpack-sources@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" - integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== + version "3.3.4" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.4.tgz#a338b95eb484ecc75fbb196cbe8a2890618b4891" + integrity sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q== webpack@^5.74.0: - version "5.105.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.0.tgz#38b5e6c5db8cbe81debbd16e089335ada05ea23a" - integrity sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw== + version "5.105.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.2.tgz#f3b76f9fc36f1152e156e63ffda3bbb82e6739ea" + integrity sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw== dependencies: "@types/eslint-scope" "^3.7.7" "@types/estree" "^1.0.8"
{{ lot.description }} {% if lot.storageLocation %} {{ helper.structural_entity_link(lot.storageLocation) }} {% else %} - + {% trans %}part_lots.location_unknown{% endtrans %} {% endif %} {% if lot.instockUnknown %} - + {% trans %}part_lots.instock_unknown{% endtrans %} {% else %} {{ lot.amount | format_amount(part.partUnit, {'decimals': 5}) }} {% endif %} -
- {% if lot.owner %} - +
+ {% if lot.owner %} + {{ helper.user_icon_link(lot.owner) }} -
- {% endif %} - {% if lot.expirationDate %} - + + {% endif %} + {% if lot.expirationDate %} + {{ lot.expirationDate | format_date() }}
- {% endif %} - {% if lot.expired %} -
- + {% endif %} + {% if lot.expired %} + {% trans %}part_lots.is_expired{% endtrans %} - {% endif %} - {% if lot.needsRefill %} -
- - - {% trans %}part_lots.need_refill{% endtrans %} - - {% endif %} - + {% endif %} + {% if lot.needsRefill %} + + + {% trans %}part_lots.need_refill{% endtrans %} + + {% endif %} + {% if lot.lastStocktakeAt %} + + + {{ lot.lastStocktakeAt | format_datetime("short") }} + + {% endif %}
@@ -90,12 +94,15 @@ > + +
{{ dropdown.profile_dropdown('part_lot', lot.id, false) }} {# Action for order information #}