diff --git a/composer.json b/composer.json
index 36dd461e..dd12097c 100644
--- a/composer.json
+++ b/composer.json
@@ -90,6 +90,7 @@
"symfony/yaml": "7.4.*",
"symplify/easy-coding-standard": "^12.5.20",
"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",
diff --git a/composer.lock b/composer.lock
index b56a61f9..9ccb922e 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": "b47c8579efa64349c5e65296e2a44206",
"packages": [
{
"name": "amphp/amp",
@@ -16830,6 +16830,54 @@
],
"time": "2025-05-14T06:15:44+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",
"version": "v2.4.0",
diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php
index 38130f0d..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;
@@ -89,7 +90,7 @@ trait AdvancedPropertyTrait
*/
#[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])]
#[ORM\Column(type: Types::STRING, nullable: true)]
- #[Length(max: 14)]
+ #[ValidGTIN]
protected ?string $gtin = null;
/**
diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php
index b8276589..902aff40 100644
--- a/src/Form/Part/PartBaseType.php
+++ b/src/Form/Part/PartBaseType.php
@@ -216,7 +216,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, [
diff --git a/src/Validator/Constraints/ValidGTIN.php b/src/Validator/Constraints/ValidGTIN.php
new file mode 100644
index 00000000..20e81f1d
--- /dev/null
+++ b/src/Validator/Constraints/ValidGTIN.php
@@ -0,0 +1,35 @@
+.
+ */
+
+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/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/tests/Validator/Constraints/ValidGTINValidatorTest.php b/tests/Validator/Constraints/ValidGTINValidatorTest.php
new file mode 100644
index 00000000..f35b053a
--- /dev/null
+++ b/tests/Validator/Constraints/ValidGTINValidatorTest.php
@@ -0,0 +1,72 @@
+.
+ */
+
+namespace App\Tests\Validator\Constraints;
+
+use App\Validator\Constraints\ValidGTINValidator;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Validator\ConstraintValidatorInterface;
+use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
+
+class ValidGTINValidatorTest extends ConstraintValidatorTestCase
+{
+
+ public function testAllowNull(): void
+ {
+ $this->validator->validate(null, new \App\Validator\Constraints\ValidGTIN());
+ $this->assertNoViolation();
+ }
+
+ public function testValidGTIN8(): void
+ {
+ $this->validator->validate('12345670', new \App\Validator\Constraints\ValidGTIN());
+ $this->assertNoViolation();
+ }
+
+ public function testValidGTIN12(): void
+ {
+ $this->validator->validate('123456789012', new \App\Validator\Constraints\ValidGTIN());
+ $this->assertNoViolation();
+ }
+
+ public function testValidGTIN13(): void
+ {
+ $this->validator->validate('1234567890128', new \App\Validator\Constraints\ValidGTIN());
+ $this->assertNoViolation();
+ }
+
+ public function testValidGTIN14(): void
+ {
+ $this->validator->validate('12345678901231', new \App\Validator\Constraints\ValidGTIN());
+ $this->assertNoViolation();
+ }
+
+ public function testInvalidGTIN(): void
+ {
+ $this->validator->validate('1234567890123', new \App\Validator\Constraints\ValidGTIN());
+ $this->buildViolation('validator.invalid_gtin')
+ ->assertRaised();
+ }
+
+ protected function createValidator(): ConstraintValidatorInterface
+ {
+ return new ValidGTINValidator();
+ }
+}
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf
index 5692ed25..66053133 100644
--- a/translations/messages.en.xlf
+++ b/translations/messages.en.xlf
@@ -12401,5 +12401,11 @@ Buerklin-API Authentication server:
Update to
+
+
+ part.gtin
+ GTIN / EAN
+
+
-
\ No newline at end of file
+
diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf
index e2e70d03..ed824f0b 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
+