From 654c2ed2af8422eaa68923dcff5ba978e0dac856 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 29 Sep 2025 13:54:13 +0200 Subject: [PATCH] IPN-Vorschlagslogik erweitert und Bauteil-IPN vereindeutigt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die IPN-Logik wurde um eine Konfiguration zur automatischen Suffix-Anfügung und die Berücksichtigung von doppelten Beschreibungen bei Bedarf ergänzt. Zudem wurde das Datenmodell angepasst, um eine eindeutige Speicherung der IPN zu gewährleisten. --- .../elements/ipn_suggestion_controller.js | 8 +- config/services.yaml | 2 +- docs/configuration.md | 6 +- migrations/Version20250325073036.php | 32 ++----- src/Controller/PartController.php | 2 +- src/Controller/TypeaheadController.php | 2 +- .../PartTraits/AdvancedPropertyTrait.php | 2 +- .../UserSystem/PartUniqueIpnSubscriber.php | 95 ++++++++++++------- src/Form/Part/PartBaseType.php | 25 +++-- src/Repository/PartRepository.php | 42 ++++---- .../MiscSettings/IpnSuggestSettings.php | 12 ++- .../Constraints/UniquePartIpnConstraint.php | 16 ++-- .../Constraints/UniquePartIpnValidator.php | 2 +- translations/messages.cs.xlf | 12 ++- translations/messages.de.xlf | 12 ++- translations/messages.en.xlf | 12 ++- 16 files changed, 165 insertions(+), 117 deletions(-) diff --git a/assets/controllers/elements/ipn_suggestion_controller.js b/assets/controllers/elements/ipn_suggestion_controller.js index e7289a91..c8b543cb 100644 --- a/assets/controllers/elements/ipn_suggestion_controller.js +++ b/assets/controllers/elements/ipn_suggestion_controller.js @@ -184,7 +184,7 @@ export default class extends Controller { if (categoryField) { categoryField.addEventListener("change", () => { const categoryId = Number(categoryField.value); - const description = String(descriptionField.value); + const description = String(descriptionField?.value ?? ''); // Check whether the category has changed compared to the previous ID if (categoryId !== this.previousCategoryId) { @@ -203,7 +203,7 @@ export default class extends Controller { if (descriptionField) { descriptionField.addEventListener("input", () => { const categoryId = Number(categoryField.value); - const description = String(descriptionField.value); + const description = String(descriptionField?.value ?? ''); // Check whether the description has changed compared to the previous one if (description !== this.previousDescription) { @@ -219,7 +219,7 @@ export default class extends Controller { const partId = this.partIdValue; const truncatedDescription = description.length > 150 ? description.substring(0, 150) : description; const encodedDescription = this.base64EncodeUtf8(truncatedDescription); - const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}&description=${encodedDescription}`; + const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}` + (description !== '' ? `&description=${encodedDescription}` : ''); fetch(url, { method: "GET", @@ -247,4 +247,4 @@ export default class extends Controller { const utf8Bytes = new TextEncoder().encode(text); return btoa(String.fromCharCode(...utf8Bytes)); }; -} \ No newline at end of file +} diff --git a/config/services.yaml b/config/services.yaml index f78f5209..1af529a8 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -238,7 +238,7 @@ services: App\EventSubscriber\UserSystem\PartUniqueIpnSubscriber: tags: - - { name: doctrine.event_subscriber } + - { name: doctrine.event_listener, event: onFlush, connection: default } App\Validator\Constraints\UniquePartIpnValidator: tags: [ 'validator.constraint_validator' ] diff --git a/docs/configuration.md b/docs/configuration.md index 8f48940f..3f832958 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -116,12 +116,14 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept value should be handled as confidential data and not shared publicly. * `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the part image gallery -* `IPN_ENABLE_UNIQUE_CHECK`: Set this value to false, if you want to allow users to enter a already available IPN for a part entry. - In this case a unique increment is appended to the user input. +* `IPN_AUTO_APPEND_SUFFIX`: When enabled, an incremental suffix will be added to the user input when entering an existing +* IPN again upon saving. * `IPN_SUGGEST_PART_DIGITS`: Defines the fixed number of digits used as the increment at the end of an IPN (Internal Part Number). IPN prefixes, maintained within part categories and their hierarchy, form the foundation for suggesting complete IPNs. These suggestions become accessible during IPN input of a part. The constant specifies the digits used to calculate and assign unique increments for parts within a category hierarchy, ensuring consistency and uniqueness in IPN generation. +* `IPN_USE_DUPLICATE_DESCRIPTION`: When enabled, the part’s description is used to find existing parts with the same + description and to determine the next available IPN by incrementing their numeric suffix for the suggestion list. ### E-Mail settings (all env only) diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php index 0070bcbe..3bae80ab 100644 --- a/migrations/Version20250325073036.php +++ b/migrations/Version20250325073036.php @@ -19,9 +19,6 @@ final class Version20250325073036 extends AbstractMultiPlatformMigration $this->addSql(<<<'SQL' ALTER TABLE categories ADD COLUMN part_ipn_prefix VARCHAR(255) NOT NULL DEFAULT '' SQL); - $this->addSql(<<<'SQL' - DROP INDEX UNIQ_6940A7FE3D721C14 ON parts - SQL); } public function mySQLDown(Schema $schema): void @@ -29,16 +26,13 @@ final class Version20250325073036 extends AbstractMultiPlatformMigration $this->addSql(<<<'SQL' ALTER TABLE categories DROP part_ipn_prefix SQL); - $this->addSql(<<<'SQL' - CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn) - SQL); } public function sqLiteUp(Schema $schema): void { $this->addSql(<<<'SQL' - CREATE TEMPORARY TABLE __temp__categories AS - SELECT + CREATE TEMPORARY TABLE __temp__categories AS + SELECT id, parent_id, id_preview_attachment, @@ -123,7 +117,7 @@ final class Version20250325073036 extends AbstractMultiPlatformMigration eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol - ) SELECT + ) SELECT id, parent_id, id_preview_attachment, @@ -164,17 +158,13 @@ final class Version20250325073036 extends AbstractMultiPlatformMigration $this->addSql(<<<'SQL' CREATE INDEX category_idx_parent_name ON categories (parent_id, name) SQL); - - $this->addSql(<<<'SQL' - DROP INDEX UNIQ_6940A7FE3D721C14 - SQL); } public function sqLiteDown(Schema $schema): void { $this->addSql(<<<'SQL' - CREATE TEMPORARY TABLE __temp__categories AS - SELECT + CREATE TEMPORARY TABLE __temp__categories AS + SELECT id, parent_id, id_preview_attachment, @@ -258,7 +248,7 @@ final class Version20250325073036 extends AbstractMultiPlatformMigration eda_info_exclude_from_board, eda_info_exclude_from_sim, eda_info_kicad_symbol - ) SELECT + ) SELECT id, parent_id, id_preview_attachment, @@ -299,10 +289,6 @@ final class Version20250325073036 extends AbstractMultiPlatformMigration $this->addSql(<<<'SQL' CREATE INDEX category_idx_parent_name ON categories (parent_id, name) SQL); - - $this->addSql(<<<'SQL' - CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON "parts" (ipn) - SQL); } public function postgreSQLUp(Schema $schema): void @@ -310,9 +296,6 @@ final class Version20250325073036 extends AbstractMultiPlatformMigration $this->addSql(<<<'SQL' ALTER TABLE categories ADD part_ipn_prefix VARCHAR(255) DEFAULT '' NOT NULL SQL); - $this->addSql(<<<'SQL' - DROP INDEX uniq_6940a7fe3d721c14 - SQL); } public function postgreSQLDown(Schema $schema): void @@ -320,8 +303,5 @@ final class Version20250325073036 extends AbstractMultiPlatformMigration $this->addSql(<<<'SQL' ALTER TABLE "categories" DROP part_ipn_prefix SQL); - $this->addSql(<<<'SQL' - CREATE UNIQUE INDEX uniq_6940a7fe3d721c14 ON "parts" (ipn) - SQL); } } diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 6e9d8bc7..3a121ad2 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -452,7 +452,7 @@ final class PartController extends AbstractController $template, [ 'part' => $new_part, - 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, base64_encode($data->getDescription()), $this->ipnSuggestSettings->suggestPartDigits), + 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, $data->getDescription(), $this->ipnSuggestSettings->suggestPartDigits), 'form' => $form, 'merge_old_name' => $merge_infos['tname_before'] ?? null, 'merge_other' => $merge_infos['other_part'] ?? null, diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index fe4f029f..39821f59 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -198,7 +198,7 @@ class TypeaheadController extends AbstractController $partId = null; } $categoryId = $request->query->getInt('categoryId'); - $description = $request->query->getString('description'); + $description = base64_decode($request->query->getString('description'), true); /** @var Part $part */ $part = $partId !== null ? $entityManager->getRepository(Part::class)->find($partId) : new Part(); diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index 5605ef59..1cce0bbf 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -63,7 +63,7 @@ trait AdvancedPropertyTrait */ #[Assert\Length(max: 100)] #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] - #[ORM\Column(type: Types::STRING, length: 100, nullable: true)] + #[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)] #[Length(max: 100)] #[UniquePartIpnConstraint] protected ?string $ipn = null; diff --git a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php index 498a9e88..ecc25b4f 100644 --- a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php +++ b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php @@ -5,14 +5,12 @@ namespace App\EventSubscriber\UserSystem; use App\Entity\Parts\Part; use App\Settings\MiscSettings\IpnSuggestSettings; use Doctrine\Common\EventSubscriber; -use Doctrine\Persistence\Event\LifecycleEventArgs; use Doctrine\ORM\Events; -use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\OnFlushEventArgs; class PartUniqueIpnSubscriber implements EventSubscriber { public function __construct( - private EntityManagerInterface $entityManager, private IpnSuggestSettings $ipnSuggestSettings ) { } @@ -20,55 +18,80 @@ class PartUniqueIpnSubscriber implements EventSubscriber public function getSubscribedEvents(): array { return [ - Events::prePersist, - Events::preUpdate, + Events::onFlush, ]; } - public function prePersist(LifecycleEventArgs $args): void + public function onFlush(OnFlushEventArgs $args): void { - $entity = $args->getObject(); - - if ($entity instanceof Part) { - $this->ensureUniqueIpn($entity); - } - } - - public function preUpdate(LifecycleEventArgs $args): void - { - $entity = $args->getObject(); - - if ($entity instanceof Part) { - $this->ensureUniqueIpn($entity); - } - } - - private function ensureUniqueIpn(Part $part): void - { - if ($part->getIpn() === null || $part->getIpn() === '') { + if (!$this->ipnSuggestSettings->autoAppendSuffix) { return; } - $existingPart = $this->entityManager - ->getRepository(Part::class) - ->findOneBy(['ipn' => $part->getIpn()]); + $em = $args->getObjectManager(); + $uow = $em->getUnitOfWork(); + $meta = $em->getClassMetadata(Part::class); - if ($existingPart && $existingPart->getId() !== $part->getId()) { - if ($this->ipnSuggestSettings->enableUniqueCheck) { + // Collect all IPNs already reserved in the current flush (so new entities do not collide with each other) + $reservedIpns = []; + + // Helper to assign a collision-free IPN for a Part entity + $ensureUnique = function (Part $part) use ($em, $uow, $meta, &$reservedIpns) { + $ipn = $part->getIpn(); + if ($ipn === null || $ipn === '') { return; } - // Anhang eines Inkrements bis ein einzigartiger Wert gefunden wird + // Check against IPNs already reserved in the current flush (except itself) + $originalIpn = $ipn; + $candidate = $originalIpn; $increment = 1; - $originalIpn = $part->getIpn(); - while ($this->entityManager - ->getRepository(Part::class) - ->findOneBy(['ipn' => $originalIpn . "_$increment"])) { + $conflicts = function (string $candidate) use ($em, $part, $reservedIpns) { + // Collision within the current flush session? + if (isset($reservedIpns[$candidate]) && $reservedIpns[$candidate] !== $part) { + return true; + } + // Collision with an existing DB row? + $existing = $em->getRepository(Part::class)->findOneBy(['ipn' => $candidate]); + return $existing !== null && $existing->getId() !== $part->getId(); + }; + + while ($conflicts($candidate)) { + $candidate = $originalIpn . '_' . $increment; $increment++; } - $part->setIpn($originalIpn . "_$increment"); + if ($candidate !== $ipn) { + $before = $part->getIpn(); + $part->setIpn($candidate); + + // Recompute the change set so Doctrine writes the change + $uow->recomputeSingleEntityChangeSet($meta, $part); + $reservedIpns[$candidate] = $part; + + // If the old IPN was reserved already, clean it up + if ($before !== null && isset($reservedIpns[$before]) && $reservedIpns[$before] === $part) { + unset($reservedIpns[$before]); + } + } else { + // Candidate unchanged, but reserve it so subsequent entities see it + $reservedIpns[$candidate] = $part; + } + }; + + // 1) Iterate over new entities + foreach ($uow->getScheduledEntityInsertions() as $entity) { + if ($entity instanceof Part) { + $ensureUnique($entity); + } + } + + // 2) Iterate over updates (if IPN changed, ensure uniqueness again) + foreach ($uow->getScheduledEntityUpdates() as $entity) { + if ($entity instanceof Part) { + $ensureUnique($entity); + } } } } diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index c493f12b..9cab2f9a 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -41,6 +41,7 @@ use App\Form\Type\StructuralEntityType; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\LogSystem\EventCommentNeededHelper; use App\Services\LogSystem\EventCommentType; +use App\Settings\MiscSettings\IpnSuggestSettings; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; @@ -56,8 +57,12 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class PartBaseType extends AbstractType { - public function __construct(protected Security $security, protected UrlGeneratorInterface $urlGenerator, protected EventCommentNeededHelper $event_comment_needed_helper) - { + public function __construct( + protected Security $security, + protected UrlGeneratorInterface $urlGenerator, + protected EventCommentNeededHelper $event_comment_needed_helper, + protected IpnSuggestSettings $ipnSuggestSettings, + ) { } public function buildForm(FormBuilderInterface $builder, array $options): void @@ -69,6 +74,16 @@ class PartBaseType extends AbstractType /** @var PartDetailDTO|null $dto */ $dto = $options['info_provider_dto']; + $descriptionAttr = [ + 'placeholder' => 'part.edit.description.placeholder', + 'rows' => 2, + ]; + + if ($this->ipnSuggestSettings->useDuplicateDescription) { + // Only add attribute when duplicate description feature is enabled + $descriptionAttr['data-ipn-suggestion'] = 'descriptionField'; + } + //Common section $builder ->add('name', TextType::class, [ @@ -83,11 +98,7 @@ class PartBaseType extends AbstractType 'empty_data' => '', 'label' => 'part.edit.description', 'mode' => 'markdown-single_line', - 'attr' => [ - 'placeholder' => 'part.edit.description.placeholder', - 'rows' => 2, - 'data-ipn-suggestion' => 'descriptionField', - ], + 'attr' => $descriptionAttr, ]) ->add('minAmount', SIUnitType::class, [ 'attr' => [ diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index 23318635..6974d254 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -25,6 +25,7 @@ namespace App\Repository; use App\Entity\Parts\Category; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; +use App\Settings\MiscSettings\IpnSuggestSettings; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; @@ -37,14 +38,17 @@ use Doctrine\ORM\EntityManagerInterface; class PartRepository extends NamedDBElementRepository { private TranslatorInterface $translator; + private IpnSuggestSettings $ipnSuggestSettings; public function __construct( EntityManagerInterface $em, - TranslatorInterface $translator + TranslatorInterface $translator, + IpnSuggestSettings $ipnSuggestSettings, ) { parent::__construct($em, $em->getClassMetadata(Part::class)); $this->translator = $translator; + $this->ipnSuggestSettings = $ipnSuggestSettings; } /** @@ -98,8 +102,7 @@ class PartRepository extends NamedDBElementRepository ->where('ILIKE(part.name, :query) = TRUE') ->orWhere('ILIKE(part.description, :query) = TRUE') ->orWhere('ILIKE(category.name, :query) = TRUE') - ->orWhere('ILIKE(footprint.name, :query) = TRUE') - ; + ->orWhere('ILIKE(footprint.name, :query) = TRUE'); $qb->setParameter('query', '%'.$query.'%'); @@ -117,7 +120,7 @@ class PartRepository extends NamedDBElementRepository * the part's current category and its hierarchy. If the part is unsaved, a default "n.a." prefix is returned. * * @param Part $part The part for which autocomplete suggestions are generated. - * @param string $description Base64-encoded description to assist in generating suggestions. + * @param string $description description to assist in generating suggestions. * @param int $suggestPartDigits The number of digits used in autocomplete increments. * * @return array An associative array containing the following keys: @@ -128,24 +131,13 @@ class PartRepository extends NamedDBElementRepository { $category = $part->getCategory(); $ipnSuggestions = ['commonPrefixes' => [], 'prefixesPartIncrement' => []]; - $description = base64_decode($description, true); if (strlen($description) > 150) { $description = substr($description, 0, 150); } - // Validate the category and ensure it's an instance of Category - if ($category instanceof Category) { - $currentPath = $category->getPartIpnPrefix(); - $directIpnPrefixEmpty = $category->getPartIpnPrefix() === ''; - $currentPath = $currentPath === '' ? 'n.a.' : $currentPath; - - $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $suggestPartDigits); - - $ipnSuggestions['commonPrefixes'][] = [ - 'title' => $currentPath . '-', - 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category') - ]; + if ($description !== '' && $this->ipnSuggestSettings->useDuplicateDescription) { + // Check if the description is already used in another part, $suggestionByDescription = $this->getIpnSuggestByDescription($description); @@ -162,6 +154,20 @@ class PartRepository extends NamedDBElementRepository 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.increment') ]; } + } + + // Validate the category and ensure it's an instance of Category + if ($category instanceof Category) { + $currentPath = $category->getPartIpnPrefix(); + $directIpnPrefixEmpty = $category->getPartIpnPrefix() === ''; + $currentPath = $currentPath === '' ? 'n.a.' : $currentPath; + + $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $suggestPartDigits); + + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => $currentPath . '-', + 'description' => $directIpnPrefixEmpty ? $this->translator->trans('part.edit.tab.advanced.ipn.prefix_empty.direct_category', ['%name%' => $category->getName()]) : $this->translator->trans('part.edit.tab.advanced.ipn.prefix.direct_category') + ]; $ipnSuggestions['prefixesPartIncrement'][] = [ 'title' => $currentPath . '-' . $increment, @@ -331,7 +337,7 @@ class PartRepository extends NamedDBElementRepository // Find the basic format (the IPN without suffix) from the first IPN $baseIpn = $givenIpns[0] ?? ''; - $baseIpn = preg_replace('/_\d+$/', '', $baseIpn); // Entferne vorhandene "_" + $baseIpn = preg_replace('/_\d+$/', '', $baseIpn); // Remove existing "_ " if ($baseIpn === '') { return null; diff --git a/src/Settings/MiscSettings/IpnSuggestSettings.php b/src/Settings/MiscSettings/IpnSuggestSettings.php index 1ef94b2f..96efcc33 100644 --- a/src/Settings/MiscSettings/IpnSuggestSettings.php +++ b/src/Settings/MiscSettings/IpnSuggestSettings.php @@ -39,10 +39,10 @@ class IpnSuggestSettings use SettingsTrait; #[SettingsParameter( - label: new TM("settings.misc.ipn_suggest.enableUniqueCheck"), - envVar: "bool:IPN_ENABLE_UNIQUE_CHECK", envVarMode: EnvVarMode::OVERWRITE, + label: new TM("settings.misc.ipn_suggest.autoAppendSuffix"), + envVar: "bool:IPN_AUTO_APPEND_SUFFIX", envVarMode: EnvVarMode::OVERWRITE, )] - public bool $enableUniqueCheck = true; + public bool $autoAppendSuffix = true; #[SettingsParameter(label: new TM("settings.misc.ipn_suggest.suggestPartDigits"), description: new TM("settings.misc.ipn_suggest.suggestPartDigits.help"), @@ -51,4 +51,10 @@ class IpnSuggestSettings )] #[Assert\Range(min: 1, max: 6)] public int $suggestPartDigits = 4; + + #[SettingsParameter( + label: new TM("settings.misc.ipn_suggest.useDuplicateDescription"), + envVar: "bool:IPN_USE_DUPLICATE_DESCRIPTION", envVarMode: EnvVarMode::OVERWRITE, + )] + public bool $useDuplicateDescription = false; } diff --git a/src/Validator/Constraints/UniquePartIpnConstraint.php b/src/Validator/Constraints/UniquePartIpnConstraint.php index 13fd0330..ca32f9ef 100644 --- a/src/Validator/Constraints/UniquePartIpnConstraint.php +++ b/src/Validator/Constraints/UniquePartIpnConstraint.php @@ -2,19 +2,21 @@ namespace App\Validator\Constraints; +use Attribute; use Symfony\Component\Validator\Constraint; -/** - * @Annotation - * @Target({"PROPERTY"}) - */ -#[\Attribute(\Attribute::TARGET_PROPERTY)] +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)] class UniquePartIpnConstraint extends Constraint { - public string $message = 'part.ipn.must_be_unique'; + public string $message = 'part.ipn.must_be_unique'; + + public function getTargets(): string|array + { + return [self::CLASS_CONSTRAINT, self::PROPERTY_CONSTRAINT]; + } public function validatedBy(): string { return UniquePartIpnValidator::class; } -} \ No newline at end of file +} diff --git a/src/Validator/Constraints/UniquePartIpnValidator.php b/src/Validator/Constraints/UniquePartIpnValidator.php index edee1190..5dbcafbe 100644 --- a/src/Validator/Constraints/UniquePartIpnValidator.php +++ b/src/Validator/Constraints/UniquePartIpnValidator.php @@ -25,7 +25,7 @@ class UniquePartIpnValidator extends ConstraintValidator return; } - if (!$this->ipnSuggestSettings->enableUniqueCheck) { + if ($this->ipnSuggestSettings->autoAppendSuffix) { return; } diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index b8d24325..ea232228 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -13059,10 +13059,10 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Seznam návrhů IPN součástek - + - settings.misc.ipn_suggest.enableUniqueCheck - Kontrola jedinečnosti IPN aktivní. Odznačte, pokud chcete při opětovném zadání existujícího IPN při ukládání přidat k uživatelskému vstupu inkrementální číslo. + settings.misc.ipn_suggest.autoAppendSuffix + Pokud je tato možnost povolena, bude při opětovném zadání existujícího IPN při ukládání k vstupu přidána přírůstková přípona. @@ -13071,6 +13071,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Počet čísel pro inkrement + + + settings.misc.ipn_suggest.useDuplicateDescription + Je-li povoleno, použije se popis součástky k nalezení existujících součástek se stejným popisem a k určení další volné IPN navýšením její číselné přípony pro seznam návrhů. + + settings.misc.ipn_suggest.suggestPartDigits.help diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 3ea13fdc..f316ae1d 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -13139,10 +13139,10 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Bauteil IPN-Vorschlagsliste - + - settings.misc.ipn_suggest.enableUniqueCheck - Check auf Eindeutigkeit der IPN aktiv. Deselektieren Sie, wenn Sie bei erneuter Eingabe einer vorhandenen IPN eine inkrementelle Zahl an die Benutzereingabe beim Speichern erhalten möchten. + settings.misc.ipn_suggest.autoAppendSuffix + Wenn diese Option aktiviert ist, wird der Eingabe ein inkrementelles Suffix hinzugefügt, wenn eine vorhandene IPN beim Speichern erneut eingegeben wird. @@ -13151,6 +13151,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Stellen für numerisches Inkrement + + + settings.misc.ipn_suggest.useDuplicateDescription + Wenn aktiviert, wird die Bauteil-Beschreibung verwendet, um vorhandene Teile mit derselben Beschreibung zu finden und die nächste verfügbare IPN für die Vorschlagsliste zu ermitteln, indem der numerische Suffix entsprechend erhöht wird. + + settings.misc.ipn_suggest.suggestPartDigits.help diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 82dad84f..bee33d30 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13140,10 +13140,10 @@ Please note, that you can not impersonate a disabled user. If you try you will g Part IPN Suggest - + - settings.misc.ipn_suggest.enableUniqueCheck - IPN uniqueness check active. Deselect if you want an incremental number to be added to the user input when entering an existing IPN again upon saving. + settings.misc.ipn_suggest.autoAppendSuffix + Do you want an incremental number to be added to the user input when entering an existing IPN again upon saving? @@ -13152,6 +13152,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g Increment Digits + + + settings.misc.ipn_suggest.useDuplicateDescription + When enabled, the part’s description is used to find existing parts with the same description and to determine the next available IPN by incrementing their numeric suffix for the suggestion list. + + settings.misc.ipn_suggest.suggestPartDigits.help