From 38a2af9ce1a93d2f9b682f114bff163a71e325f2 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 1 Apr 2025 16:10:10 +0200 Subject: [PATCH 01/10] =?UTF-8?q?Erweiterungst=C3=A4tigkeiten=20zur=20IPN-?= =?UTF-8?q?Vorschlagsliste=20anhand=20von=20Pr=C3=A4fixen=20aus=20den=20Ka?= =?UTF-8?q?tegorien?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 5 + .../elements/ipn_suggestion_controller.js | 220 ++++++++++++++++++ config/parameters.yaml | 2 + config/services.yaml | 24 ++ docs/configuration.md | 6 + migrations/Version20250325073036.php | 23 ++ src/Controller/PartController.php | 5 +- src/Controller/TypeaheadController.php | 31 ++- src/Entity/Parts/Category.php | 17 ++ src/Entity/Parts/Part.php | 2 - .../PartTraits/AdvancedPropertyTrait.php | 4 +- .../UserSystem/PartUniqueIpnSubscriber.php | 73 ++++++ src/Form/AdminPages/CategoryAdminForm.php | 11 + src/Form/Part/PartBaseType.php | 8 + src/Repository/PartRepository.php | 119 ++++++++++ .../Constraints/UniquePartIpnConstraint.php | 20 ++ .../Constraints/UniquePartIpnValidator.php | 38 +++ templates/admin/category_admin.html.twig | 1 + templates/parts/edit/_advanced.html.twig | 11 +- translations/messages.cs.xlf | 66 ++++++ translations/messages.da.xlf | 66 ++++++ translations/messages.de.xlf | 66 ++++++ translations/messages.el.xlf | 66 ++++++ translations/messages.en.xlf | 66 ++++++ translations/messages.es.xlf | 66 ++++++ translations/messages.fr.xlf | 68 +++++- translations/messages.it.xlf | 66 ++++++ translations/messages.ja.xlf | 66 ++++++ translations/messages.nl.xlf | 66 ++++++ translations/messages.pl.xlf | 66 ++++++ translations/messages.ru.xlf | 66 ++++++ translations/messages.zh.xlf | 66 ++++++ 32 files changed, 1472 insertions(+), 8 deletions(-) create mode 100644 assets/controllers/elements/ipn_suggestion_controller.js create mode 100644 migrations/Version20250325073036.php create mode 100644 src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php create mode 100644 src/Validator/Constraints/UniquePartIpnConstraint.php create mode 100644 src/Validator/Constraints/UniquePartIpnValidator.php diff --git a/.env b/.env index 982d4bbd..869d4154 100644 --- a/.env +++ b/.env @@ -50,6 +50,8 @@ EMAIL_SENDER_EMAIL=noreply@partdb.changeme EMAIL_SENDER_NAME="Part-DB Mailer" # Set this to 1 to allow reset of a password per email ALLOW_EMAIL_PW_RESET=0 +# Set this to 0 to allow to enter already available IPN. In this case a unique increment is appended to the user input. +ENFORCE_UNIQUE_IPN=1 ################################################################################### # Error pages settings @@ -116,6 +118,9 @@ REDIRECT_TO_HTTPS=0 # Set this to zero, if you want to disable the year 2038 bug check on 32-bit systems (it will cause errors with current 32-bit PHP versions) DISABLE_YEAR2038_BUG_CHECK=0 +# Define the number of digits used for the incremental numbering of parts in the IPN (Internal Part Number) autocomplete system. +AUTOCOMPLETE_PART_DIGITS=4 + # Set the trusted IPs here, when using an reverse proxy #TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 #TRUSTED_HOSTS='^(localhost|example\.com)$' diff --git a/assets/controllers/elements/ipn_suggestion_controller.js b/assets/controllers/elements/ipn_suggestion_controller.js new file mode 100644 index 00000000..088c07b3 --- /dev/null +++ b/assets/controllers/elements/ipn_suggestion_controller.js @@ -0,0 +1,220 @@ +import { Controller } from "@hotwired/stimulus"; +import "../../css/components/autocomplete_bootstrap_theme.css"; + +export default class extends Controller { + static targets = ["input"]; + static values = { + partId: Number, + partCategoryId: Number, + suggestions: Object, + commonSectionHeader: String, // Dynamic header for common Prefixes + partIncrementHeader: String, // Dynamic header for new possible part increment + suggestUrl: String, + }; + + connect() { + this.configureAutocomplete(); + this.watchCategoryChanges(); + } + + templates = { + commonSectionHeader({ title, html }) { + return html` +
+
+ ${title} +
+
+
+ `; + }, + partIncrementHeader({ title, html }) { + return html` +
+
+ ${title} +
+
+
+ `; + }, + list({ html }) { + return html` + + `; + }, + item({ suggestion, description, html }) { + return html` +
  • +
    +
    +
    + + + +
    +
    +
    ${suggestion}
    +
    ${description}
    +
    +
    +
    +
  • + `; + }, + }; + + configureAutocomplete() { + const inputField = this.inputTarget; + const commonPrefixes = this.suggestionsValue.commonPrefixes || []; + const prefixesPartIncrement = this.suggestionsValue.prefixesPartIncrement || []; + const commonHeader = this.commonSectionHeaderValue; + const partIncrementHeader = this.partIncrementHeaderValue; + + if (!inputField || (!commonPrefixes.length && !prefixesPartIncrement.length)) return; + + // Check whether the panel should be created at the update + if (this.isPanelInitialized) { + const existingPanel = inputField.parentNode.querySelector(".aa-Panel"); + if (existingPanel) { + // Only remove the panel in the update phase + + existingPanel.remove(); + } + } + + // Create panel + const panel = document.createElement("div"); + panel.classList.add("aa-Panel"); + panel.style.display = "none"; + + // Create panel layout + const panelLayout = document.createElement("div"); + panelLayout.classList.add("aa-PanelLayout", "aa-Panel--scrollable"); + + // Section for common prefixes + if (commonPrefixes.length) { + const commonSection = document.createElement("section"); + commonSection.classList.add("aa-Source"); + + const commonSectionHeader = this.templates.commonSectionHeader({ + title: commonHeader, + html: String.raw, + }); + commonSection.innerHTML += commonSectionHeader; + + const commonList = document.createElement("ul"); + commonList.classList.add("aa-List"); + commonList.setAttribute("role", "listbox"); + + commonPrefixes.forEach((prefix) => { + const itemHTML = this.templates.item({ + suggestion: prefix.title, + description: prefix.description, + html: String.raw, + }); + commonList.innerHTML += itemHTML; + }); + + commonSection.appendChild(commonList); + panelLayout.appendChild(commonSection); + } + + // Section for prefixes part increment + if (prefixesPartIncrement.length) { + const partIncrementSection = document.createElement("section"); + partIncrementSection.classList.add("aa-Source"); + + const partIncrementHeaderHtml = this.templates.partIncrementHeader({ + title: partIncrementHeader, + html: String.raw, + }); + partIncrementSection.innerHTML += partIncrementHeaderHtml; + + const partIncrementList = document.createElement("ul"); + partIncrementList.classList.add("aa-List"); + partIncrementList.setAttribute("role", "listbox"); + + prefixesPartIncrement.forEach((prefix) => { + const itemHTML = this.templates.item({ + suggestion: prefix.title, + description: prefix.description, + html: String.raw, + }); + partIncrementList.innerHTML += itemHTML; + }); + + partIncrementSection.appendChild(partIncrementList); + panelLayout.appendChild(partIncrementSection); + } + + panel.appendChild(panelLayout); + inputField.parentNode.appendChild(panel); + + inputField.addEventListener("focus", () => { + panel.style.display = "block"; + }); + + inputField.addEventListener("blur", () => { + setTimeout(() => { + panel.style.display = "none"; + }, 100); + }); + + // Selection of an item + panelLayout.addEventListener("mousedown", (event) => { + const target = event.target.closest("li"); + + if (target) { + inputField.value = target.dataset.suggestion; + panel.style.display = "none"; + } + }); + + this.isPanelInitialized = true; + }; + + watchCategoryChanges() { + const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]'); + this.previousCategoryId = Number(this.partCategoryIdValue); + + if (categoryField) { + categoryField.addEventListener("change", () => { + const categoryId = Number(categoryField.value); + + // Check whether the category has changed compared to the previous ID + if (categoryId !== this.previousCategoryId) { + this.fetchNewSuggestions(categoryId); + this.previousCategoryId = categoryId; + } + }); + } + } + + fetchNewSuggestions(categoryId) { + const baseUrl = this.suggestUrlValue; + const partId = this.partIdValue; + const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}`; + + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error(`Error when calling up the IPN-suggestions: ${response.status}`); + } + return response.json(); + }) + .then((data) => { + this.suggestionsValue = data; + this.configureAutocomplete(); + }) + .catch((error) => { + console.error("Errors when loading the new IPN-suggestions:", error); + }); + }; +} \ No newline at end of file diff --git a/config/parameters.yaml b/config/parameters.yaml index 5b40899d..30c38957 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -9,6 +9,7 @@ parameters: # This is used as workaround for places where we can not access the settings directly (like the 2FA application names) partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage) partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl'] # The languages that are shown in user drop down menu + partdb.autocomplete_part_digits: '%env(trim:string:AUTOCOMPLETE_PART_DIGITS)%' # The number of digits used for the incremental numbering of parts in the IPN (Internal Part Number) autocomplete system. partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails @@ -19,6 +20,7 @@ parameters: ###################################################################################################################### partdb.gdpr_compliance: true # If this option is activated, IP addresses are anonymized to be GDPR compliant partdb.users.email_pw_reset: '%env(bool:ALLOW_EMAIL_PW_RESET)%' # Config if users are able, to reset their password by email. By default this enabled, when a mail server is configured. + partdb.users.enforce_unique_ipn: '%env(bool:ENFORCE_UNIQUE_IPN)%' # Config if users are able, to enter an already available IPN. In this case a unique increment is appended to the user input. ###################################################################################################################### # Mail settings diff --git a/config/services.yaml b/config/services.yaml index 17611cea..fa70e87c 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -231,6 +231,30 @@ services: tags: - { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' } + App\Controller\PartController: + bind: + $autocompletePartDigits: '%partdb.autocomplete_part_digits%' + + App\Controller\TypeaheadController: + bind: + $autocompletePartDigits: '%partdb.autocomplete_part_digits%' + + App\Repository\PartRepository: + arguments: + $translator: '@translator' + tags: ['doctrine.repository_service'] + + App\EventSubscriber\UserSystem\PartUniqueIpnSubscriber: + arguments: + $enforceUniqueIpn: '%partdb.users.enforce_unique_ipn%' + tags: + - { name: doctrine.event_subscriber } + + App\Validator\Constraints\UniquePartIpnValidator: + arguments: + $enforceUniqueIpn: '%partdb.users.enforce_unique_ipn%' + tags: [ 'validator.constraint_validator' ] + # We are needing this service inside a migration, where only the container is injected. So we need to define it as public, to access it from the container. App\Services\UserSystem\PermissionPresetsHelper: public: true diff --git a/docs/configuration.md b/docs/configuration.md index d4b21781..0292242c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -116,6 +116,10 @@ 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 +* `AUTOCOMPLETE_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. ### E-Mail settings (all env only) @@ -128,6 +132,8 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept sent from. * `ALLOW_EMAIL_PW_RESET`: Set this value to true, if you want to allow users to reset their password via an email notification. You have to configure the mail provider first before via the MAILER_DSN setting. +* `ENFORCE_UNIQUE_IPN`: 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. ### Table related settings diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php new file mode 100644 index 00000000..8c346b6b --- /dev/null +++ b/migrations/Version20250325073036.php @@ -0,0 +1,23 @@ +addSql('ALTER TABLE categories ADD part_ipn_prefix VARCHAR(255) NOT NULL AFTER partname_regex'); + $this->addSql('DROP INDEX UNIQ_6940A7FE3D721C14 ON parts'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE `categories` DROP part_ipn_prefix'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON `parts` (ipn)'); + } +} diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index aeb2664e..92ad8b86 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -74,6 +74,7 @@ final class PartController extends AbstractController private readonly EntityManagerInterface $em, private readonly EventCommentHelper $commentHelper, private readonly PartInfoSettings $partInfoSettings, + private readonly int $autocompletePartDigits, ) { } @@ -444,10 +445,13 @@ final class PartController extends AbstractController $template = 'parts/edit/update_from_ip.html.twig'; } + $partRepository = $this->em->getRepository(Part::class); + return $this->render( $template, [ 'part' => $new_part, + 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, $this->autocompletePartDigits), 'form' => $form, 'merge_old_name' => $merge_infos['tname_before'] ?? null, 'merge_other' => $merge_infos['other_part'] ?? null, @@ -457,7 +461,6 @@ final class PartController extends AbstractController ); } - #[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/TypeaheadController.php b/src/Controller/TypeaheadController.php index 89eac7ff..76dbf3d0 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -60,8 +60,11 @@ use Symfony\Component\Serializer\Serializer; #[Route(path: '/typeahead')] class TypeaheadController extends AbstractController { - public function __construct(protected AttachmentURLGenerator $urlGenerator, protected Packages $assets) - { + public function __construct( + protected AttachmentURLGenerator $urlGenerator, + protected Packages $assets, + protected int $autocompletePartDigits + ) { } #[Route(path: '/builtInResources/search', name: 'typeahead_builtInRessources')] @@ -183,4 +186,28 @@ class TypeaheadController extends AbstractController return new JsonResponse($data, Response::HTTP_OK, [], true); } + + #[Route(path: '/parts/ipn-suggestions', name: 'ipn_suggestions', methods: ['GET'])] + public function ipnSuggestions( + Request $request, + EntityManagerInterface $entityManager + ): JsonResponse { + $partId = $request->query->get('partId'); + if ($partId === '0' || $partId === 'undefined' || $partId === 'null') { + $partId = null; + } + $categoryId = $request->query->getInt('categoryId'); + + /** @var Part $part */ + $part = $partId !== null ? $entityManager->getRepository(Part::class)->find($partId) : new Part(); + $category = $entityManager->getRepository(Category::class)->find($categoryId); + + $clonedPart = clone $part; + $clonedPart->setCategory($category); + + $partRepository = $entityManager->getRepository(Part::class); + $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $this->autocompletePartDigits); + + return new JsonResponse($ipnSuggestions); + } } diff --git a/src/Entity/Parts/Category.php b/src/Entity/Parts/Category.php index 99ed3c6d..7d2e0d1e 100644 --- a/src/Entity/Parts/Category.php +++ b/src/Entity/Parts/Category.php @@ -118,6 +118,13 @@ class Category extends AbstractPartsContainingDBElement #[ORM\Column(type: Types::TEXT)] protected string $partname_regex = ''; + /** + * @var string The prefix for ipn generation for created parts in this category. + */ + #[Groups(['full', 'import', 'category:read', 'category:write'])] + #[ORM\Column(type: Types::STRING, length: 255, nullable: false)] + protected string $part_ipn_prefix = ''; + /** * @var bool Set to true, if the footprints should be disabled for parts this category (not implemented yet). */ @@ -225,6 +232,16 @@ class Category extends AbstractPartsContainingDBElement return $this; } + public function getPartIpnPrefix(): string + { + return $this->part_ipn_prefix; + } + + public function setPartIpnPrefix(string $part_ipn_prefix): void + { + $this->part_ipn_prefix = $part_ipn_prefix; + } + public function isDisableFootprints(): bool { return $this->disable_footprints; diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 2f274a8a..d6eff737 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -61,7 +61,6 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping as ORM; -use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -75,7 +74,6 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; * @extends AttachmentContainingDBElement * @template-use ParametersTrait */ -#[UniqueEntity(fields: ['ipn'], message: 'part.ipn.must_be_unique')] #[ORM\Entity(repositoryClass: PartRepository::class)] #[ORM\EntityListeners([TreeCacheInvalidationListener::class])] #[ORM\Table('`parts`')] diff --git a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php index 230ba7b7..5605ef59 100644 --- a/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php @@ -29,6 +29,7 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints\Length; +use App\Validator\Constraints\UniquePartIpnConstraint; /** * Advanced properties of a part, not related to a more specific group. @@ -62,8 +63,9 @@ trait AdvancedPropertyTrait */ #[Assert\Length(max: 100)] #[Groups(['extended', 'full', 'import', 'part:read', 'part:write'])] - #[ORM\Column(type: Types::STRING, length: 100, unique: true, nullable: true)] + #[ORM\Column(type: Types::STRING, length: 100, nullable: true)] #[Length(max: 100)] + #[UniquePartIpnConstraint] protected ?string $ipn = null; /** diff --git a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php new file mode 100644 index 00000000..9cff3166 --- /dev/null +++ b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php @@ -0,0 +1,73 @@ +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() === '') { + return; + } + + $existingPart = $this->entityManager + ->getRepository(Part::class) + ->findOneBy(['ipn' => $part->getIpn()]); + + if ($existingPart && $existingPart->getId() !== $part->getId()) { + if ($this->enforceUniqueIpn) { + return; + } + + // Anhang eines Inkrements bis ein einzigartiger Wert gefunden wird + $increment = 1; + $originalIpn = $part->getIpn(); + + while ($this->entityManager + ->getRepository(Part::class) + ->findOneBy(['ipn' => $originalIpn . "_$increment"])) { + $increment++; + } + + $part->setIpn($originalIpn . "_$increment"); + } + } +} \ No newline at end of file diff --git a/src/Form/AdminPages/CategoryAdminForm.php b/src/Form/AdminPages/CategoryAdminForm.php index 44c1dede..489649ed 100644 --- a/src/Form/AdminPages/CategoryAdminForm.php +++ b/src/Form/AdminPages/CategoryAdminForm.php @@ -84,6 +84,17 @@ class CategoryAdminForm extends BaseEntityAdminForm 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); + $builder->add('part_ipn_prefix', TextType::class, [ + 'required' => false, + 'empty_data' => '', + 'label' => 'category.edit.part_ipn_prefix', + 'help' => 'category.edit.part_ipn_prefix.help', + 'attr' => [ + 'placeholder' => 'category.edit.part_ipn_prefix.placeholder', + ], + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), + ]); + $builder->add('default_description', RichTextEditorType::class, [ 'required' => false, 'empty_data' => '', diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index 0bd3d0e3..06639bf3 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -104,6 +104,9 @@ class PartBaseType extends AbstractType 'disable_not_selectable' => true, //Do not require category for new parts, so that the user must select the category by hand and cannot forget it (the requirement is handled by the constraint in the entity) 'required' => !$new_part, + 'attr' => [ + 'data-ipn-suggestion' => 'categoryField', + ] ]) ->add('footprint', StructuralEntityType::class, [ 'class' => Footprint::class, @@ -175,6 +178,11 @@ class PartBaseType extends AbstractType 'required' => false, 'empty_data' => null, 'label' => 'part.edit.ipn', + 'attr' => [ + 'class' => 'ipn-suggestion-field', + 'data-elements--ipn-suggestion-target' => 'input', + 'autocomplete' => 'off', + ] ]); //Comment section diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index edccd74b..cdba4f77 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -22,17 +22,31 @@ declare(strict_types=1); namespace App\Repository; +use App\Entity\Parts\Category; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NoResultException; use Doctrine\ORM\QueryBuilder; +use Symfony\Contracts\Translation\TranslatorInterface; +use Doctrine\ORM\EntityManagerInterface; /** * @extends NamedDBElementRepository */ class PartRepository extends NamedDBElementRepository { + private TranslatorInterface $translator; + + public function __construct( + EntityManagerInterface $em, + TranslatorInterface $translator + ) { + parent::__construct($em, $em->getClassMetadata(Part::class)); + + $this->translator = $translator; + } + /** * Gets the summed up instock of all parts (only parts without a measurement unit). * @@ -94,4 +108,109 @@ class PartRepository extends NamedDBElementRepository return $qb->getQuery()->getResult(); } + + public function autoCompleteIpn(Part $part, int $autocompletePartDigits): array + { + $category = $part->getCategory(); + $ipnSuggestions = ['commonPrefixes' => [], 'prefixesPartIncrement' => []]; + + // 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, $autocompletePartDigits); + + $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, + '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.increment') + ]; + + // Process parent categories + $parentCategory = $category->getParent(); + + while ($parentCategory instanceof Category) { + // Prepend the parent category's prefix to the current path + $currentPath = $parentCategory->getPartIpnPrefix() . '-' . $currentPath; + $currentPath = $parentCategory->getPartIpnPrefix() === '' ? 'n.a.-' . $currentPath : $currentPath; + + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => $currentPath . '-', + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment') + ]; + + $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $autocompletePartDigits); + + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $currentPath . '-' . $increment, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.hierarchical.increment') + ]; + + // Move to the next parent category + $parentCategory = $parentCategory->getParent(); + } + } elseif ($part->getID() === null) { + $ipnSuggestions['commonPrefixes'][] = [ + 'title' => 'n.a.', + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.not_saved') + ]; + } + + return $ipnSuggestions; + } + + public function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $autocompletePartDigits): string + { + $qb = $this->createQueryBuilder('part'); + + $expectedLength = strlen($currentPath) + 1 + $autocompletePartDigits; // Path + '-' + $autocompletePartDigits digits + + // Fetch all parts in the given category, sorted by their ID in ascending order + $qb->select('part') + ->where('part.ipn LIKE :ipnPattern') + ->andWhere('LENGTH(part.ipn) = :expectedLength') + ->setParameter('ipnPattern', $currentPath . '%') + ->setParameter('expectedLength', $expectedLength) + ->orderBy('part.id', 'ASC'); + + $parts = $qb->getQuery()->getResult(); + + // Collect all used increments in the category + $usedIncrements = []; + foreach ($parts as $part) { + if ($part->getIpn() === null || $part->getIpn() === '') { + continue; + } + + if ($part->getId() === $currentPart->getId()) { + // Extract and return the current part's increment directly + $incrementPart = substr($part->getIpn(), -$autocompletePartDigits); + if (is_numeric($incrementPart)) { + return str_pad((string) $incrementPart, $autocompletePartDigits, '0', STR_PAD_LEFT); + } + } + + // Extract last $autocompletePartDigits digits for possible available part increment + $incrementPart = substr($part->getIpn(), -$autocompletePartDigits); + if (is_numeric($incrementPart)) { + $usedIncrements[] = (int) $incrementPart; + } + + } + + // Generate the next free $autocompletePartDigits-digit increment + $nextIncrement = 1; // Start at the beginning + + while (in_array($nextIncrement, $usedIncrements)) { + $nextIncrement++; + } + + return str_pad((string) $nextIncrement, $autocompletePartDigits, '0', STR_PAD_LEFT); + } } diff --git a/src/Validator/Constraints/UniquePartIpnConstraint.php b/src/Validator/Constraints/UniquePartIpnConstraint.php new file mode 100644 index 00000000..13fd0330 --- /dev/null +++ b/src/Validator/Constraints/UniquePartIpnConstraint.php @@ -0,0 +1,20 @@ +entityManager = $entityManager; + $this->enforceUniqueIpn = $enforceUniqueIpn; + } + + public function validate($value, Constraint $constraint) + { + if (null === $value || '' === $value) { + return; + } + + $repository = $this->entityManager->getRepository(Part::class); + $existingPart = $repository->findOneBy(['ipn' => $value]); + + if ($existingPart) { + if ($this->enforceUniqueIpn) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $value) + ->addViolation(); + } + } + } +} \ No newline at end of file diff --git a/templates/admin/category_admin.html.twig b/templates/admin/category_admin.html.twig index 5811640b..d87cee7f 100644 --- a/templates/admin/category_admin.html.twig +++ b/templates/admin/category_admin.html.twig @@ -31,6 +31,7 @@
    {{ form_row(form.partname_regex) }} {{ form_row(form.partname_hint) }} + {{ form_row(form.part_ipn_prefix) }}
    {{ form_row(form.default_description) }} {{ form_row(form.default_comment) }} diff --git a/templates/parts/edit/_advanced.html.twig b/templates/parts/edit/_advanced.html.twig index 12b546ab..4dd91dd1 100644 --- a/templates/parts/edit/_advanced.html.twig +++ b/templates/parts/edit/_advanced.html.twig @@ -1,5 +1,14 @@ {{ form_row(form.needsReview) }} {{ form_row(form.favorite) }} {{ form_row(form.mass) }} -{{ form_row(form.ipn) }} +
    + {{ form_row(form.ipn) }} +
    {{ form_row(form.partUnit) }} \ No newline at end of file diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 1f234450..b2763b71 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -1842,6 +1842,54 @@ Související prvky budou přesunuty nahoru. Pokročilé + + + part.edit.tab.advanced.ipn.commonSectionHeader + Návrhy bez přírůstku části + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Návrhy s číselnými přírůstky částí + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN předpona přímé kategorie je prázdná, zadejte ji v kategorii „%name%“ + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN prefix přímé kategorie + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN prefix přímé kategorie a specifického přírůstku pro část + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN prefixy s hierarchickým pořadím kategorií rodičovských prefixů + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN prefixy s hierarchickým pořadím kategorií rodičovských prefixů a specifickým přírůstkem pro část + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Nejprve vytvořte součást a přiřaďte ji do kategorie: s dostupnými kategoriemi a jejich vlastními IPN prefixy lze automaticky navrhnout IPN označení pro danou součást + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6959,6 +7007,12 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn Filtr názvů + + + category.edit.part_ipn_prefix + Předpona součásti IPN + + obsolete @@ -10254,12 +10308,24 @@ Element 3 např. "/Kondenzátor \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + např. "B12A" + + category.edit.partname_regex.help Regulární výraz kompatibilní s PCRE, kterému musí název dílu odpovídat. + + + category.edit.part_ipn_prefix.help + Předpona navrhovaná při zadávání IPN části. + + entity.select.add_hint diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index d7258986..0510fa16 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -1850,6 +1850,54 @@ Underelementer vil blive flyttet opad. Advanceret + + + part.edit.tab.advanced.ipn.commonSectionHeader + Forslag uden del-inkrement + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Forslag med numeriske deleforøgelser + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN-præfikset for den direkte kategori er tomt, angiv det i kategorien "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN-præfiks for direkte kategori + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN-præfiks for den direkte kategori og en delspecifik inkrement + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-præfikser med hierarkisk rækkefølge af overordnede præfikser + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-præfikser med hierarkisk rækkefølge af overordnede præfikser og en del-specifik inkrement + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Opret først en komponent, og tildel den en kategori: med eksisterende kategorier og deres egne IPN-præfikser kan IPN-betegnelsen for komponenten foreslås automatisk + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6966,6 +7014,12 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt Navnefilter + + + category.edit.part_ipn_prefix + IPN-komponentförstavelse + + obsolete @@ -10280,12 +10334,24 @@ Element 3 f.eks. "/Kondensator \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + f.eks. "B12A" + + category.edit.partname_regex.help Et PCRE-kompatibelt regulært udtryk, som delnavnet skal opfylde. + + + category.edit.part_ipn_prefix.help + Et prefix foreslået, når IPN for en del indtastes. + + entity.select.add_hint diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 06326a21..34421bf8 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1841,6 +1841,54 @@ Subelemente werden beim Löschen nach oben verschoben. Erweiterte Optionen + + + part.edit.tab.advanced.ipn.commonSectionHeader + Vorschläge ohne Teil-Inkrement + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Vorschläge mit numerischen Teil-Inkrement + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN-Präfix der direkten Kategorie leer, geben Sie einen Präfix in Kategorie "%name%" an + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN-Präfix der direkten Kategorie + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN-Präfix der direkten Kategorie und eines teilspezifischen Inkrements + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-Präfixe mit hierarchischer Kategorienreihenfolge der Elternpräfixe + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-Präfixe mit hierarchischer Kategorienreihenfolge der Elternpräfixe und ein teilsspezifisches Inkrement + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Bitte erstellen Sie zuerst ein Bauteil und weisen Sie dieses einer Kategorie zu: mit vorhandenen Kategorien und derene eigenen IPN-Präfix kann die IPN-Angabe für das jeweilige Teil automatisch vorgeschlagen werden + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6958,6 +7006,12 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr Namensfilter + + + category.edit.part_ipn_prefix + Bauteil IPN-Präfix + + obsolete @@ -10328,12 +10382,24 @@ Element 1 -> Element 1.2 z.B. "/Kondensator \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + z.B. "B12A" + + category.edit.partname_regex.help Ein PCRE-kompatibler regulärer Ausdruck, den der Bauteilename erfüllen muss. + + + category.edit.part_ipn_prefix.help + Ein Präfix, der bei der IPN-Eingabe eines Bauteils vorgeschlagen wird. + + entity.select.add_hint diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index cc17d9be..02703ca7 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1535,5 +1535,71 @@ Επεξεργασία + + + category.edit.part_ipn_prefix + Πρόθεμα εξαρτήματος IPN + + + + + category.edit.part_ipn_prefix.placeholder + π.χ. "B12A" + + + + + category.edit.part_ipn_prefix.help + Μια προτεινόμενη πρόθεμα κατά την εισαγωγή του IPN ενός τμήματος. + + + + + part.edit.tab.advanced.ipn.commonSectionHeader + Προτάσεις χωρίς αύξηση μέρους + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Προτάσεις με αριθμητικές αυξήσεις μερών + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Το IPN πρόθεμα της άμεσης κατηγορίας είναι κενό, καθορίστε το στην κατηγορία "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Πρόθεμα IPN για την άμεση κατηγορία + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Πρόθεμα IPN της άμεσης κατηγορίας και μιας ειδικής για μέρος αύξησης + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Προθέματα IPN με ιεραρχική σειρά κατηγοριών των προθέτων γονέων + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Προθέματα IPN με ιεραρχική σειρά κατηγοριών των προθέτων γονέων και συγκεκριμένη αύξηση για το μέρος + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Δημιουργήστε πρώτα ένα εξάρτημα και αντιστοιχίστε το σε μια κατηγορία: με τις υπάρχουσες κατηγορίες και τα δικά τους προθέματα IPN, η ονομασία IPN για το εξάρτημα μπορεί να προταθεί αυτόματα + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index a2ec2f65..3dbbcf1c 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -1842,6 +1842,54 @@ Sub elements will be moved upwards. Advanced + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggestions without part increment + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Suggestions with numeric part increment + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + IPN prefix of direct category empty, specify one in category "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN prefix of direct category + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN prefix of direct category and part-specific increment + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN prefixes with hierarchical category order of parent-prefix(es) + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN prefixes with hierarchical category order of parent-prefix(es) and part-specific increment + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Please create part at first and assign it to a category: with existing categories and their own IPN prefix, the IPN for the part can be suggested automatically + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6959,6 +7007,12 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Name filter + + + category.edit.part_ipn_prefix + Part IPN Prefix + + obsolete @@ -10329,12 +10383,24 @@ Element 1 -> Element 1.2 e.g "/Capacitor \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + e.g "B12A" + + category.edit.partname_regex.help A PCRE-compatible regular expression, which a part name have to match. + + + category.edit.part_ipn_prefix.help + A prefix suggested when entering the IPN of a part. + + entity.select.add_hint diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index fce38e52..0a2be1bd 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -1842,6 +1842,54 @@ Subelementos serán desplazados hacia arriba. Avanzado + + + part.edit.tab.advanced.ipn.commonSectionHeader + Sugerencias sin incremento de parte + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Sugerencias con incrementos numéricos de partes + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + El prefijo IPN de la categoría directa está vacío, especifíquelo en la categoría "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Prefijo IPN de la categoría directa + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Prefijo IPN de la categoría directa y un incremento específico de la pieza + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Prefijos IPN con orden jerárquico de categorías de prefijos principales + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Prefijos IPN con orden jerárquico de categorías de prefijos principales y un incremento específico para la parte + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Primero cree un componente y asígnele una categoría: con las categorías existentes y sus propios prefijos IPN, el identificador IPN para el componente puede ser sugerido automáticamente + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6958,6 +7006,12 @@ Subelementos serán desplazados hacia arriba. Filtro de nombre + + + category.edit.part_ipn_prefix + Prefijo de IPN de la pieza + + obsolete @@ -10272,12 +10326,24 @@ Elemento 3 p.ej. "/Condensador \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + p.ej. "B12A" + + category.edit.partname_regex.help Una expresión regular compatible con PCRE, la cual debe coincidir con el nombre de un componente. + + + category.edit.part_ipn_prefix.help + Un prefijo sugerido al ingresar el IPN de una parte. + + entity.select.add_hint diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 292dbafa..0e3aee01 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -1820,6 +1820,54 @@ Show/Hide sidebar Avancé + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggestions sans incrément de partie + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Propositions avec incréments numériques de parties + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Le préfixe IPN de la catégorie directe est vide, veuillez le spécifier dans la catégorie "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Préfixe IPN de la catégorie directe + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Préfixe IPN de la catégorie directe et d'un incrément spécifique à la partie + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Préfixes IPN avec un ordre hiérarchique des catégories des préfixes parents + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Préfixes IPN avec un ordre hiérarchique des catégories des préfixes parents et un incrément spécifique à la pièce + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Créez d'abord une pièce et assignez-la à une catégorie : avec les catégories existantes et leurs propres préfixes IPN, l'identifiant IPN pour la pièce peut être proposé automatiquement + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6900,6 +6948,12 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia Filtre de nom + + + category.edit.part_ipn_prefix + Préfixe de pièce IPN + + obsolete @@ -6947,7 +7001,7 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia company.edit.address.placeholder - Ex. 99 exemple de rue + Ex. 99 exemple de rue exemple de ville @@ -9097,5 +9151,17 @@ exemple de ville Si vous avez des questions à propos de Part-DB , rendez vous sur <a href="%href%" class="link-external" target="_blank">Github</a> + + + category.edit.part_ipn_prefix.placeholder + par ex. "B12A" + + + + + category.edit.part_ipn_prefix.help + Un préfixe suggéré lors de la saisie de l'IPN d'une pièce. + + diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 828304eb..41b20e49 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -1842,6 +1842,54 @@ I sub elementi saranno spostati verso l'alto. Avanzate + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggerimenti senza incremento di parte + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Suggerimenti con incrementi numerici delle parti + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Il prefisso IPN della categoria diretta è vuoto, specificarlo nella categoria "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Prefisso IPN della categoria diretta + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Prefisso IPN della categoria diretta e di un incremento specifico della parte + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Prefissi IPN con ordine gerarchico delle categorie dei prefissi padre + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Prefissi IPN con ordine gerarchico delle categorie dei prefissi padre e un incremento specifico per il pezzo + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Crea prima un componente e assegnagli una categoria: con le categorie esistenti e i loro propri prefissi IPN, l'identificativo IPN per il componente può essere suggerito automaticamente + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6960,6 +7008,12 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi Filtro nome + + + category.edit.part_ipn_prefix + Prefisso parte IPN + + obsolete @@ -10274,12 +10328,24 @@ Element 3 es. "/Condensatore \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + es. "B12A" + + category.edit.partname_regex.help Un'espressione regolare compatibile con PCRE che il nome del componente deve soddisfare. + + + category.edit.part_ipn_prefix.help + Un prefisso suggerito durante l'inserimento dell'IPN di una parte. + + entity.select.add_hint diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 4becc319..b9e5ddf9 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -1820,6 +1820,54 @@ 詳細 + + + part.edit.tab.advanced.ipn.commonSectionHeader + 部品の増加なしの提案。 + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + パーツの数値インクリメントを含む提案 + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + 直接カテゴリの IPN プレフィックスが空です。「%name%」カテゴリで指定してください + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + 直接カテゴリのIPNプレフィックス + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + 直接カテゴリのIPNプレフィックスと部品特有のインクリメント + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + 親プレフィックスの階層カテゴリ順のIPNプレフィックス + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + 親プレフィックスの階層カテゴリ順とパーツ固有の増分のIPNプレフィックス + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + まずはコンポーネントを作成し、それをカテゴリに割り当ててください:既存のカテゴリとそれぞれのIPNプレフィックスを基に、コンポーネントのIPNを自動的に提案できます + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6901,6 +6949,12 @@ 名前のフィルター + + + category.edit.part_ipn_prefix + 部品 IPN 接頭辞 + + obsolete @@ -8834,5 +8888,17 @@ Exampletown Part-DBについての質問は、<a href="%href%" class="link-external" target="_blank">GitHub</a> にスレッドがあります。 + + + category.edit.part_ipn_prefix.placeholder + 例: "B12A" + + + + + category.edit.part_ipn_prefix.help + 部品のIPN入力時に提案される接頭辞。 + + diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 760533d7..99cbd706 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -724,5 +724,71 @@ Weet u zeker dat u wilt doorgaan? + + + category.edit.part_ipn_prefix + IPN-voorvoegsel van onderdeel + + + + + category.edit.part_ipn_prefix.placeholder + bijv. "B12A" + + + + + category.edit.part_ipn_prefix.help + Een voorgesteld voorvoegsel bij het invoeren van de IPN van een onderdeel. + + + + + part.edit.tab.advanced.ipn.commonSectionHeader + Suggesties zonder toename van onderdelen + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Suggesties met numerieke verhogingen van onderdelen + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Het IPN-prefix van de directe categorie is leeg, geef het op in de categorie "%name%" + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + IPN-prefix van de directe categorie + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + IPN-voorvoegsel van de directe categorie en een onderdeel specifiek increment + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-prefixen met een hiërarchische volgorde van hoofdcategorieën + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-prefixen met een hiërarchische volgorde van hoofdcategorieën en een specifieke toename voor het onderdeel + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Maak eerst een component en wijs het toe aan een categorie: met de bestaande categorieën en hun eigen IPN-prefixen kan de IPN voor het component automatisch worden voorgesteld + + diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index b769e273..07907095 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -1847,6 +1847,54 @@ Po usunięciu pod elementy zostaną przeniesione na górę. Zaawansowane + + + part.edit.tab.advanced.ipn.commonSectionHeader + Sugestie bez zwiększenia części + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Propozycje z numerycznymi przyrostami części + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Prefiks IPN kategorii bezpośredniej jest pusty, podaj go w kategorii "%name%". + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Prefiks IPN kategorii bezpośredniej + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Prefiks IPN bezpośredniej kategorii i specyficzny dla części przyrost + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + Prefiksy IPN z hierarchiczną kolejnością kategorii prefiksów nadrzędnych + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + Prefiksy IPN z hierarchiczną kolejnością kategorii prefiksów nadrzędnych i specyficznym przyrostem dla części + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Najpierw utwórz komponent i przypisz go do kategorii: dzięki istniejącym kategoriom i ich własnym prefiksom IPN identyfikator IPN dla komponentu może być proponowany automatycznie + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6963,6 +7011,12 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo Filtr nazwy + + + category.edit.part_ipn_prefix + Prefiks IPN części + + obsolete @@ -10277,12 +10331,24 @@ Element 3 np. "/Kondensator \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + np. "B12A" + + category.edit.partname_regex.help Wyrażenie regularne zgodne z PCRE, do którego musi pasować nazwa komponentu. + + + category.edit.part_ipn_prefix.help + Een voorgesteld voorvoegsel bij het invoeren van de IPN van een onderdeel. + + entity.select.add_hint diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 62570acb..4d5e2d34 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -1850,6 +1850,54 @@ Расширенные + + + part.edit.tab.advanced.ipn.commonSectionHeader + Предложения без увеличения частей. + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + Предложения с числовыми приращениями частей + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + Префикс IPN для прямой категории пуст, укажите его в категории «%name%». + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + Префикс IPN для прямой категории + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + Префикс IPN прямой категории и специфическое для части приращение + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + IPN-префиксы с иерархическим порядком категорий родительских префиксов + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + IPN-префиксы с иерархическим порядком категорий родительских префиксов и специфическим увеличением для компонента + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + Сначала создайте компонент и назначьте ему категорию: на основе существующих категорий и их собственных IPN-префиксов идентификатор IPN для компонента может быть предложен автоматически + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6970,6 +7018,12 @@ Фильтр по имени + + + category.edit.part_ipn_prefix + Префикс IPN детали + + obsolete @@ -10281,12 +10335,24 @@ e.g "/Конденсатор \d+ nF/i" + + + category.edit.part_ipn_prefix.placeholder + e.g "B12A" + + category.edit.partname_regex.help PCRE-совместимое регулярное выражение которому должно соответствовать имя компонента. + + + category.edit.part_ipn_prefix.help + Предлагаемый префикс при вводе IPN детали. + + entity.select.add_hint diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index 668c32f2..fb178f85 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -1850,6 +1850,54 @@ 高级 + + + part.edit.tab.advanced.ipn.commonSectionHeader + Sugestie bez zwiększenia części + + + + + part.edit.tab.advanced.ipn.partIncrementHeader + 包含部件数值增量的建议 + + + + + part.edit.tab.advanced.ipn.prefix_empty.direct_category + 直接类别的 IPN 前缀为空,请在类别“%name%”中指定。 + + + + + part.edit.tab.advanced.ipn.prefix.direct_category + 直接类别的IPN前缀 + + + + + part.edit.tab.advanced.ipn.prefix.direct_category.increment + 直接类别的IPN前缀和部件特定的增量 + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment + 具有父级前缀层级类别顺序的IPN前缀 + + + + + part.edit.tab.advanced.ipn.prefix.hierarchical.increment + 具有父级前缀层级类别顺序和组件特定增量的IPN前缀 + + + + + part.edit.tab.advanced.ipn.prefix.not_saved + 请先创建组件并将其分配到类别:基于现有类别及其专属的IPN前缀,可以自动建议组件的IPN + + Part-DB1\templates\Parts\edit\edit_part_info.html.twig:40 @@ -6967,6 +7015,12 @@ 名称过滤器 + + + category.edit.part_ipn_prefix + 部件 IPN 前缀 + + obsolete @@ -10280,12 +10334,24 @@ Element 3 + + + category.edit.part_ipn_prefix.placeholder + 例如:"B12A" + + category.edit.partname_regex.help 与PCRE兼容的正则表达式,部分名称必须匹配。 + + + category.edit.part_ipn_prefix.help + 输入零件IPN时建议的前缀。 + + entity.select.add_hint From 378489d30a25648bd71b2b44206cac518dc1cba0 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 23 Apr 2025 13:59:23 +0200 Subject: [PATCH 02/10] =?UTF-8?q?Umstellung=20Migrationen=20bzgl.=20Multi-?= =?UTF-8?q?Plattform-Support.=20Zun=C3=A4chst=20MySQL,=20SQLite=20Statemen?= =?UTF-8?q?ts=20integrieren.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/Version20250325073036.php | 288 ++++++++++++++++++++++++++- src/Entity/Parts/Category.php | 2 +- 2 files changed, 284 insertions(+), 6 deletions(-) diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php index 8c346b6b..1d5bdfd2 100644 --- a/migrations/Version20250325073036.php +++ b/migrations/Version20250325073036.php @@ -4,20 +4,298 @@ declare(strict_types=1); namespace DoctrineMigrations; +use App\Migration\AbstractMultiPlatformMigration; use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; -final class Version20250325073036 extends AbstractMigration +final class Version20250325073036 extends AbstractMultiPlatformMigration { - public function up(Schema $schema): void + public function getDescription(): string { - $this->addSql('ALTER TABLE categories ADD part_ipn_prefix VARCHAR(255) NOT NULL AFTER partname_regex'); + return 'Add part_ipn_prefix column to categories table and remove unique constraint from parts table'; + } + + public function mySQLUp(Schema $schema): void + { + $this->addSql('ALTER TABLE categories ADD COLUMN part_ipn_prefix VARCHAR(255) NOT NULL DEFAULT \'\''); $this->addSql('DROP INDEX UNIQ_6940A7FE3D721C14 ON parts'); } - public function down(Schema $schema): void + public function mySQLDown(Schema $schema): void { $this->addSql('ALTER TABLE `categories` DROP part_ipn_prefix'); $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON `parts` (ipn)'); } + + public function sqLiteUp(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__categories AS + SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM categories + SQL); + + $this->addSql('DROP TABLE categories'); + + $this->addSql(<<<'SQL' + CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + partname_hint CLOB NOT NULL, + partname_regex CLOB NOT NULL, + part_ipn_prefix VARCHAR(255) DEFAULT '' NOT NULL, + disable_footprints BOOLEAN NOT NULL, + disable_manufacturers BOOLEAN NOT NULL, + disable_autodatasheets BOOLEAN NOT NULL, + disable_properties BOOLEAN NOT NULL, + default_description CLOB NOT NULL, + default_comment CLOB NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + alternative_names CLOB DEFAULT NULL, + eda_info_reference_prefix 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, + CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO categories ( + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + ) SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM __temp__categories + SQL); + + $this->addSql('DROP TABLE __temp__categories'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668727ACA70 ON categories (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668EA7100A1 ON categories (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_name ON categories (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_parent_name ON categories (parent_id, name) + SQL); + } + + public function sqLiteDown(Schema $schema): void + { + $this->addSql(<<<'SQL' + CREATE TEMPORARY TABLE __temp__categories AS + SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM categories + SQL); + + $this->addSql('DROP TABLE categories'); + + $this->addSql(<<<'SQL' + CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + parent_id INTEGER DEFAULT NULL, + id_preview_attachment INTEGER DEFAULT NULL, + partname_hint CLOB NOT NULL, + partname_regex CLOB NOT NULL, + disable_footprints BOOLEAN NOT NULL, + disable_manufacturers BOOLEAN NOT NULL, + disable_autodatasheets BOOLEAN NOT NULL, + disable_properties BOOLEAN NOT NULL, + default_description CLOB NOT NULL, + default_comment CLOB NOT NULL, + comment CLOB NOT NULL, + not_selectable BOOLEAN NOT NULL, + name VARCHAR(255) NOT NULL, + last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + alternative_names CLOB DEFAULT NULL, + eda_info_reference_prefix 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, + CONSTRAINT FK_3AF34668727ACA70 FOREIGN KEY (parent_id) REFERENCES categories (id) ON UPDATE NO ACTION ON DELETE NO ACTION NOT DEFERRABLE INITIALLY IMMEDIATE, + CONSTRAINT FK_3AF34668EA7100A1 FOREIGN KEY (id_preview_attachment) REFERENCES attachments (id) ON UPDATE NO ACTION ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE + ) + SQL); + + $this->addSql(<<<'SQL' + INSERT INTO categories ( + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + ) SELECT + id, + parent_id, + id_preview_attachment, + partname_hint, + partname_regex, + disable_footprints, + disable_manufacturers, + disable_autodatasheets, + disable_properties, + default_description, + default_comment, + comment, + not_selectable, + name, + last_modified, + datetime_added, + alternative_names, + eda_info_reference_prefix, + eda_info_invisible, + eda_info_exclude_from_bom, + eda_info_exclude_from_board, + eda_info_exclude_from_sim, + eda_info_kicad_symbol + FROM __temp__categories + SQL); + + $this->addSql('DROP TABLE __temp__categories'); + + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668727ACA70 ON categories (parent_id) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX IDX_3AF34668EA7100A1 ON categories (id_preview_attachment) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_name ON categories (name) + SQL); + $this->addSql(<<<'SQL' + CREATE INDEX category_idx_parent_name ON categories (parent_id, name) + SQL); + } + + public function postgreSQLUp(Schema $schema): void + { + //Not needed + } + + public function postgreSQLDown(Schema $schema): void + { + //Not needed + } } diff --git a/src/Entity/Parts/Category.php b/src/Entity/Parts/Category.php index 7d2e0d1e..7fca81bc 100644 --- a/src/Entity/Parts/Category.php +++ b/src/Entity/Parts/Category.php @@ -122,7 +122,7 @@ class Category extends AbstractPartsContainingDBElement * @var string The prefix for ipn generation for created parts in this category. */ #[Groups(['full', 'import', 'category:read', 'category:write'])] - #[ORM\Column(type: Types::STRING, length: 255, nullable: false)] + #[ORM\Column(type: Types::STRING, length: 255, nullable: false, options: ['default' => ''])] protected string $part_ipn_prefix = ''; /** From 0586e80b9efc858a1702587177df0c1b3b30ba79 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 17 Jun 2025 11:47:01 +0200 Subject: [PATCH 03/10] Postgre Statements integrieren --- migrations/Version20250325073036.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php index 1d5bdfd2..dae163dc 100644 --- a/migrations/Version20250325073036.php +++ b/migrations/Version20250325073036.php @@ -291,11 +291,21 @@ final class Version20250325073036 extends AbstractMultiPlatformMigration public function postgreSQLUp(Schema $schema): void { - //Not needed + $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 { - //Not needed + $this->addSql(<<<'SQL' + ALTER TABLE "categories" DROP part_ipn_prefix + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX uniq_6940a7fe3d721c14 ON "parts" (ipn) + SQL); } } From a7665af6b8671b308d0eaa0eaf9c1cd9096a002a Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Tue, 17 Jun 2025 13:54:50 +0200 Subject: [PATCH 04/10] SQL-Formatierung in Migration verbessern --- migrations/Version20250325073036.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php index dae163dc..a9d3eaaa 100644 --- a/migrations/Version20250325073036.php +++ b/migrations/Version20250325073036.php @@ -16,14 +16,22 @@ final class Version20250325073036 extends AbstractMultiPlatformMigration public function mySQLUp(Schema $schema): void { - $this->addSql('ALTER TABLE categories ADD COLUMN part_ipn_prefix VARCHAR(255) NOT NULL DEFAULT \'\''); - $this->addSql('DROP INDEX UNIQ_6940A7FE3D721C14 ON parts'); + $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 { - $this->addSql('ALTER TABLE `categories` DROP part_ipn_prefix'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON `parts` (ipn)'); + $this->addSql(<<<'SQL' + ALTER TABLE categories DROP part_ipn_prefixSQL + SQL); + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn) + SQL); } public function sqLiteUp(Schema $schema): void From b2b0f39ac67ee5bf0548edfa6fa7a2ea4d670528 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 9 Jul 2025 09:45:43 +0200 Subject: [PATCH 05/10] Erweitere IPN-Suggest um Bauteilbeschreibung. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Implementierung berücksichtigt nun zusätzlich die Bauteilbeschreibung zu maximal 150 Zeichen Länge für die Generierung von IPN-Vorschlägen und Inkrementen. --- .../elements/ckeditor_controller.js | 9 ++ .../elements/ipn_suggestion_controller.js | 92 ++++++++---- src/Controller/PartController.php | 2 +- src/Controller/TypeaheadController.php | 3 +- src/Form/Part/PartBaseType.php | 1 + src/Repository/PartRepository.php | 132 +++++++++++++++++- templates/parts/edit/_advanced.html.twig | 1 + translations/messages.cs.xlf | 12 ++ translations/messages.da.xlf | 12 ++ translations/messages.de.xlf | 12 ++ translations/messages.el.xlf | 12 ++ translations/messages.en.xlf | 12 ++ translations/messages.es.xlf | 12 ++ translations/messages.fr.xlf | 12 ++ translations/messages.it.xlf | 12 ++ translations/messages.ja.xlf | 12 ++ translations/messages.nl.xlf | 12 ++ translations/messages.pl.xlf | 12 ++ translations/messages.ru.xlf | 12 ++ translations/messages.zh.xlf | 12 ++ 20 files changed, 361 insertions(+), 35 deletions(-) diff --git a/assets/controllers/elements/ckeditor_controller.js b/assets/controllers/elements/ckeditor_controller.js index 62a48b15..7f55dd5c 100644 --- a/assets/controllers/elements/ckeditor_controller.js +++ b/assets/controllers/elements/ckeditor_controller.js @@ -78,6 +78,15 @@ export default class extends Controller { editor_div.classList.add(...new_classes.split(",")); } + // Automatic synchronization of source input + editor.model.document.on("change:data", () => { + editor.updateSourceElement(); + + // Dispatch the input event for further treatment + const event = new Event("input"); + this.element.dispatchEvent(event); + }); + //This return is important! Otherwise we get mysterious errors in the console //See: https://github.com/ckeditor/ckeditor5/issues/5897#issuecomment-628471302 return editor; diff --git a/assets/controllers/elements/ipn_suggestion_controller.js b/assets/controllers/elements/ipn_suggestion_controller.js index 088c07b3..e7289a91 100644 --- a/assets/controllers/elements/ipn_suggestion_controller.js +++ b/assets/controllers/elements/ipn_suggestion_controller.js @@ -6,6 +6,7 @@ export default class extends Controller { static values = { partId: Number, partCategoryId: Number, + partDescription: String, suggestions: Object, commonSectionHeader: String, // Dynamic header for common Prefixes partIncrementHeader: String, // Dynamic header for new possible part increment @@ -15,6 +16,7 @@ export default class extends Controller { connect() { this.configureAutocomplete(); this.watchCategoryChanges(); + this.watchDescriptionChanges(); } templates = { @@ -92,34 +94,6 @@ export default class extends Controller { const panelLayout = document.createElement("div"); panelLayout.classList.add("aa-PanelLayout", "aa-Panel--scrollable"); - // Section for common prefixes - if (commonPrefixes.length) { - const commonSection = document.createElement("section"); - commonSection.classList.add("aa-Source"); - - const commonSectionHeader = this.templates.commonSectionHeader({ - title: commonHeader, - html: String.raw, - }); - commonSection.innerHTML += commonSectionHeader; - - const commonList = document.createElement("ul"); - commonList.classList.add("aa-List"); - commonList.setAttribute("role", "listbox"); - - commonPrefixes.forEach((prefix) => { - const itemHTML = this.templates.item({ - suggestion: prefix.title, - description: prefix.description, - html: String.raw, - }); - commonList.innerHTML += itemHTML; - }); - - commonSection.appendChild(commonList); - panelLayout.appendChild(commonSection); - } - // Section for prefixes part increment if (prefixesPartIncrement.length) { const partIncrementSection = document.createElement("section"); @@ -148,6 +122,34 @@ export default class extends Controller { panelLayout.appendChild(partIncrementSection); } + // Section for common prefixes + if (commonPrefixes.length) { + const commonSection = document.createElement("section"); + commonSection.classList.add("aa-Source"); + + const commonSectionHeader = this.templates.commonSectionHeader({ + title: commonHeader, + html: String.raw, + }); + commonSection.innerHTML += commonSectionHeader; + + const commonList = document.createElement("ul"); + commonList.classList.add("aa-List"); + commonList.setAttribute("role", "listbox"); + + commonPrefixes.forEach((prefix) => { + const itemHTML = this.templates.item({ + suggestion: prefix.title, + description: prefix.description, + html: String.raw, + }); + commonList.innerHTML += itemHTML; + }); + + commonSection.appendChild(commonList); + panelLayout.appendChild(commonSection); + } + panel.appendChild(panelLayout); inputField.parentNode.appendChild(panel); @@ -176,25 +178,48 @@ export default class extends Controller { watchCategoryChanges() { const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]'); + const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]'); this.previousCategoryId = Number(this.partCategoryIdValue); if (categoryField) { categoryField.addEventListener("change", () => { const categoryId = Number(categoryField.value); + const description = String(descriptionField.value); // Check whether the category has changed compared to the previous ID if (categoryId !== this.previousCategoryId) { - this.fetchNewSuggestions(categoryId); + this.fetchNewSuggestions(categoryId, description); this.previousCategoryId = categoryId; } }); } } - fetchNewSuggestions(categoryId) { + watchDescriptionChanges() { + const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]'); + const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]'); + this.previousDescription = String(this.partDescriptionValue); + + if (descriptionField) { + descriptionField.addEventListener("input", () => { + const categoryId = Number(categoryField.value); + const description = String(descriptionField.value); + + // Check whether the description has changed compared to the previous one + if (description !== this.previousDescription) { + this.fetchNewSuggestions(categoryId, description); + this.previousDescription = description; + } + }); + } + } + + fetchNewSuggestions(categoryId, description) { const baseUrl = this.suggestUrlValue; const partId = this.partIdValue; - const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}`; + const truncatedDescription = description.length > 150 ? description.substring(0, 150) : description; + const encodedDescription = this.base64EncodeUtf8(truncatedDescription); + const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}&description=${encodedDescription}`; fetch(url, { method: "GET", @@ -217,4 +242,9 @@ export default class extends Controller { console.error("Errors when loading the new IPN-suggestions:", error); }); }; + + base64EncodeUtf8(text) { + const utf8Bytes = new TextEncoder().encode(text); + return btoa(String.fromCharCode(...utf8Bytes)); + }; } \ No newline at end of file diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 92ad8b86..0c7a24e3 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -451,7 +451,7 @@ final class PartController extends AbstractController $template, [ 'part' => $new_part, - 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, $this->autocompletePartDigits), + 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, base64_encode($data->getDescription()), $this->autocompletePartDigits), '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 76dbf3d0..f1e83d21 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -197,6 +197,7 @@ class TypeaheadController extends AbstractController $partId = null; } $categoryId = $request->query->getInt('categoryId'); + $description = $request->query->getString('description'); /** @var Part $part */ $part = $partId !== null ? $entityManager->getRepository(Part::class)->find($partId) : new Part(); @@ -206,7 +207,7 @@ class TypeaheadController extends AbstractController $clonedPart->setCategory($category); $partRepository = $entityManager->getRepository(Part::class); - $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $this->autocompletePartDigits); + $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, $this->autocompletePartDigits); return new JsonResponse($ipnSuggestions); } diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index 06639bf3..c493f12b 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -86,6 +86,7 @@ class PartBaseType extends AbstractType 'attr' => [ 'placeholder' => 'part.edit.description.placeholder', 'rows' => 2, + 'data-ipn-suggestion' => 'descriptionField', ], ]) ->add('minAmount', SIUnitType::class, [ diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index cdba4f77..69361553 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -109,10 +109,30 @@ class PartRepository extends NamedDBElementRepository return $qb->getQuery()->getResult(); } - public function autoCompleteIpn(Part $part, int $autocompletePartDigits): array + /** + * Provides IPN (Internal Part Number) suggestions for a given part based on its category, description, + * and configured autocomplete digit length. + * + * This function generates suggestions for common prefixes and incremented prefixes based on + * 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 int $autocompletePartDigits The number of digits used in autocomplete increments. + * + * @return array An associative array containing the following keys: + * - 'commonPrefixes': List of common prefixes found for the part. + * - 'prefixesPartIncrement': Increments for the generated prefixes, including hierarchical prefixes. + */ + public function autoCompleteIpn(Part $part, string $description, int $autocompletePartDigits): array { $category = $part->getCategory(); $ipnSuggestions = ['commonPrefixes' => [], 'prefixesPartIncrement' => []]; + $description = base64_decode($description); + + if (strlen($description) > 150) { + $description = substr($description, 0, 150); + } // Validate the category and ensure it's an instance of Category if ($category instanceof Category) { @@ -127,6 +147,22 @@ class PartRepository extends NamedDBElementRepository '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') ]; + $suggestionByDescription = $this->getIpnSuggestByDescription($description); + + if ($suggestionByDescription !== null && $suggestionByDescription !== $part->getIpn() && $part->getIpn() !== null && $part->getIpn() !== '') { + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $part->getIpn(), + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.current-increment') + ]; + } + + if ($suggestionByDescription !== null) { + $ipnSuggestions['prefixesPartIncrement'][] = [ + 'title' => $suggestionByDescription, + 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.description.increment') + ]; + } + $ipnSuggestions['prefixesPartIncrement'][] = [ 'title' => $currentPath . '-' . $increment, '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.increment') @@ -165,7 +201,62 @@ class PartRepository extends NamedDBElementRepository return $ipnSuggestions; } - public function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $autocompletePartDigits): string + /** + * Suggests the next IPN (Internal Part Number) based on the provided part description. + * + * Searches for parts with similar descriptions and retrieves their existing IPNs to calculate the next suggestion. + * Returns null if the description is empty or no suggestion can be generated. + * + * @param string $description The part description to search for. + * + * @return string|null The suggested IPN, or null if no suggestion is possible. + * + * @throws NonUniqueResultException + */ + public function getIpnSuggestByDescription(string $description): ?string + { + if ($description === '') { + return null; + } + + $qb = $this->createQueryBuilder('part'); + + $qb->select('part') + ->where('part.description LIKE :descriptionPattern') + ->setParameter('descriptionPattern', $description.'%') + ->orderBy('part.id', 'ASC'); + + $partsBySameDescription = $qb->getQuery()->getResult(); + $givenIpnsWithSameDescription = []; + + foreach ($partsBySameDescription as $part) { + if ($part->getIpn() === null || $part->getIpn() === '') { + continue; + } + + $givenIpnsWithSameDescription[] = $part->getIpn(); + } + + return $this->getNextIpnSuggestion($givenIpnsWithSameDescription); + } + + /** + * Generates the next possible increment for a part within a given category, while ensuring uniqueness. + * + * This method calculates the next available increment for a part's identifier (`ipn`) based on the current path + * and the number of digits specified for the autocomplete feature. It ensures that the generated identifier + * aligns with the expected length and does not conflict with already existing identifiers in the same category. + * + * @param string $currentPath The base path or prefix for the part's identifier. + * @param Part $currentPart The part entity for which the increment is being generated. + * @param int $autocompletePartDigits The number of digits reserved for the increment. + * + * @return string|null The next possible increment as a zero-padded string, or null if it cannot be generated. + * + * @throws NonUniqueResultException If the query returns non-unique results. + * @throws NoResultException If the query fails to return a result. + */ + private function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $autocompletePartDigits): ?string { $qb = $this->createQueryBuilder('part'); @@ -213,4 +304,41 @@ class PartRepository extends NamedDBElementRepository return str_pad((string) $nextIncrement, $autocompletePartDigits, '0', STR_PAD_LEFT); } + + /** + * Generates the next IPN suggestion based on the maximum numeric suffix found in the given IPNs. + * + * The new IPN is constructed using the base format of the first provided IPN, + * incremented by the next free numeric suffix. If no base IPNs are found, + * returns null. + * + * @param array $givenIpns List of IPNs to analyze. + * + * @return string|null The next suggested IPN, or null if no base IPNs can be derived. + */ + private function getNextIpnSuggestion(array $givenIpns): ?string { + $maxSuffix = 0; + + foreach ($givenIpns as $ipn) { + // Check whether the IPN contains a suffix "_ " + if (preg_match('/_(\d+)$/', $ipn, $matches)) { + $suffix = (int)$matches[1]; + if ($suffix > $maxSuffix) { + $maxSuffix = $suffix; // Höchste Nummer speichern + } + } + } + + // Find the basic format (the IPN without suffix) from the first IPN + $baseIpn = $givenIpns[0] ?? ''; + $baseIpn = preg_replace('/_\d+$/', '', $baseIpn); // Entferne vorhandene "_" + + if ($baseIpn === '') { + return null; + } + + // Generate next free possible IPN + return $baseIpn . '_' . ($maxSuffix + 1); + } + } diff --git a/templates/parts/edit/_advanced.html.twig b/templates/parts/edit/_advanced.html.twig index 4dd91dd1..de31786c 100644 --- a/templates/parts/edit/_advanced.html.twig +++ b/templates/parts/edit/_advanced.html.twig @@ -4,6 +4,7 @@
    Návrhy s číselnými přírůstky částí + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuální specifikace IPN pro součást + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Další možná specifikace IPN na základě identického popisu součásti + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 0510fa16..14a6e31a 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -1862,6 +1862,18 @@ Underelementer vil blive flyttet opad. Forslag med numeriske deleforøgelser + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuel IPN-specifikation for delen + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Næste mulige IPN-specifikation baseret på en identisk delebeskrivelse + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 34421bf8..a726c3cd 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -1853,6 +1853,18 @@ Subelemente werden beim Löschen nach oben verschoben. Vorschläge mit numerischen Teil-Inkrement + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktuelle IPN-Angabe des Bauteils + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Nächstmögliche IPN-Angabe auf Basis der identischen Bauteil-Beschreibung + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 02703ca7..3473c0d1 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1565,6 +1565,18 @@ Προτάσεις με αριθμητικές αυξήσεις μερών + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Τρέχουσα προδιαγραφή IPN του εξαρτήματος + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Επόμενη δυνατή προδιαγραφή IPN βάσει της ίδιας περιγραφής εξαρτήματος + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 3dbbcf1c..a231458b 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -1854,6 +1854,18 @@ Sub elements will be moved upwards. Suggestions with numeric part increment + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Current IPN specification of the part + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Next possible IPN specification based on an identical part description + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 0a2be1bd..1282e8a3 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -1854,6 +1854,18 @@ Subelementos serán desplazados hacia arriba. Sugerencias con incrementos numéricos de partes + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Especificación actual de IPN de la pieza + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Siguiente especificación de IPN posible basada en una descripción idéntica de la pieza + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 0e3aee01..d1aed017 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -1832,6 +1832,18 @@ Show/Hide sidebar Propositions avec incréments numériques de parties + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Spécification IPN actuelle pour la pièce + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Prochaine spécification IPN possible basée sur une description identique de la pièce + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 41b20e49..16a839d8 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -1854,6 +1854,18 @@ I sub elementi saranno spostati verso l'alto. Suggerimenti con incrementi numerici delle parti + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Specifica IPN attuale per il pezzo + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Prossima specifica IPN possibile basata su una descrizione identica del pezzo + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index b9e5ddf9..86306ab3 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -1832,6 +1832,18 @@ パーツの数値インクリメントを含む提案 + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + 部品の現在のIPN仕様 + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + 同じ部品説明に基づく次の可能なIPN仕様 + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 99cbd706..e97b3649 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -754,6 +754,18 @@ Suggesties met numerieke verhogingen van onderdelen + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Huidige IPN-specificatie voor het onderdeel + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Volgende mogelijke IPN-specificatie op basis van een identieke onderdeelbeschrijving + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 07907095..43d8303d 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -1859,6 +1859,18 @@ Po usunięciu pod elementy zostaną przeniesione na górę. Propozycje z numerycznymi przyrostami części + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Aktualna specyfikacja IPN dla części + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Następna możliwa specyfikacja IPN na podstawie identycznego opisu części + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 4d5e2d34..33f6b44b 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -1862,6 +1862,18 @@ Предложения с числовыми приращениями частей + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + Текущая спецификация IPN для детали + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + Следующая возможная спецификация IPN на основе идентичного описания детали + + part.edit.tab.advanced.ipn.prefix_empty.direct_category diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index fb178f85..e8369214 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -1862,6 +1862,18 @@ 包含部件数值增量的建议 + + + part.edit.tab.advanced.ipn.prefix.description.current-increment + 部件的当前IPN规格 + + + + + part.edit.tab.advanced.ipn.prefix.description.increment + 基于相同部件描述的下一个可能的IPN规格 + + part.edit.tab.advanced.ipn.prefix_empty.direct_category From c7bc9123260d4329163e254cd7317eb02a31c38b Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 17 Jul 2025 11:07:10 +0200 Subject: [PATCH 06/10] Anpassungen aus Analyse vornehmen --- migrations/Version20250325073036.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/migrations/Version20250325073036.php b/migrations/Version20250325073036.php index a9d3eaaa..0070bcbe 100644 --- a/migrations/Version20250325073036.php +++ b/migrations/Version20250325073036.php @@ -27,7 +27,7 @@ final class Version20250325073036 extends AbstractMultiPlatformMigration public function mySQLDown(Schema $schema): void { $this->addSql(<<<'SQL' - ALTER TABLE categories DROP part_ipn_prefixSQL + ALTER TABLE categories DROP part_ipn_prefix SQL); $this->addSql(<<<'SQL' CREATE UNIQUE INDEX UNIQ_6940A7FE3D721C14 ON parts (ipn) @@ -164,6 +164,10 @@ 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 @@ -295,6 +299,10 @@ 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 From 3100c832463971649086d43d75d3c62e83aea595 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 25 Aug 2025 10:43:58 +0200 Subject: [PATCH 07/10] =?UTF-8?q?IPN-Validierung=20f=C3=BCr=20Parts=20?= =?UTF-8?q?=C3=BCberarbeiten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Constraints/UniquePartIpnValidator.php | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Validator/Constraints/UniquePartIpnValidator.php b/src/Validator/Constraints/UniquePartIpnValidator.php index 51b5402b..019202f8 100644 --- a/src/Validator/Constraints/UniquePartIpnValidator.php +++ b/src/Validator/Constraints/UniquePartIpnValidator.php @@ -24,14 +24,27 @@ class UniquePartIpnValidator extends ConstraintValidator return; } - $repository = $this->entityManager->getRepository(Part::class); - $existingPart = $repository->findOneBy(['ipn' => $value]); + if (!$this->enforceUniqueIpn) { + return; + } - if ($existingPart) { - if ($this->enforceUniqueIpn) { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $value) - ->addViolation(); + /** @var Part $currentPart */ + $currentPart = $this->context->getObject(); + + if (!$currentPart instanceof Part) { + return; + } + + $repository = $this->entityManager->getRepository(Part::class); + $existingParts = $repository->findBy(['ipn' => $value]); + + foreach ($existingParts as $existingPart) { + if ($currentPart->getId() !== $existingPart->getId()) { + if ($this->enforceUniqueIpn) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $value) + ->addViolation(); + } } } } From a2f53290f48ee1c42f8a6c34c67bd9ce2f980fe7 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Thu, 25 Sep 2025 10:26:34 +0200 Subject: [PATCH 08/10] IPN-Vorschlagslogik um Konfiguration erweitert --- .env | 5 -- config/parameters.yaml | 2 - config/services.yaml | 12 ----- docs/configuration.md | 6 +-- src/Controller/PartController.php | 5 +- src/Controller/TypeaheadController.php | 5 +- .../UserSystem/PartUniqueIpnSubscriber.php | 7 +-- src/Repository/PartRepository.php | 22 ++++---- .../MiscSettings/IpnSuggestSettings.php | 54 +++++++++++++++++++ src/Settings/MiscSettings/MiscSettings.php | 5 +- .../Constraints/UniquePartIpnValidator.php | 19 ++++--- translations/messages.cs.xlf | 24 +++++++++ translations/messages.de.xlf | 24 +++++++++ translations/messages.en.xlf | 24 +++++++++ 14 files changed, 163 insertions(+), 51 deletions(-) create mode 100644 src/Settings/MiscSettings/IpnSuggestSettings.php diff --git a/.env b/.env index 869d4154..982d4bbd 100644 --- a/.env +++ b/.env @@ -50,8 +50,6 @@ EMAIL_SENDER_EMAIL=noreply@partdb.changeme EMAIL_SENDER_NAME="Part-DB Mailer" # Set this to 1 to allow reset of a password per email ALLOW_EMAIL_PW_RESET=0 -# Set this to 0 to allow to enter already available IPN. In this case a unique increment is appended to the user input. -ENFORCE_UNIQUE_IPN=1 ################################################################################### # Error pages settings @@ -118,9 +116,6 @@ REDIRECT_TO_HTTPS=0 # Set this to zero, if you want to disable the year 2038 bug check on 32-bit systems (it will cause errors with current 32-bit PHP versions) DISABLE_YEAR2038_BUG_CHECK=0 -# Define the number of digits used for the incremental numbering of parts in the IPN (Internal Part Number) autocomplete system. -AUTOCOMPLETE_PART_DIGITS=4 - # Set the trusted IPs here, when using an reverse proxy #TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 #TRUSTED_HOSTS='^(localhost|example\.com)$' diff --git a/config/parameters.yaml b/config/parameters.yaml index 30c38957..5b40899d 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -9,7 +9,6 @@ parameters: # This is used as workaround for places where we can not access the settings directly (like the 2FA application names) partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage) partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl'] # The languages that are shown in user drop down menu - partdb.autocomplete_part_digits: '%env(trim:string:AUTOCOMPLETE_PART_DIGITS)%' # The number of digits used for the incremental numbering of parts in the IPN (Internal Part Number) autocomplete system. partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails @@ -20,7 +19,6 @@ parameters: ###################################################################################################################### partdb.gdpr_compliance: true # If this option is activated, IP addresses are anonymized to be GDPR compliant partdb.users.email_pw_reset: '%env(bool:ALLOW_EMAIL_PW_RESET)%' # Config if users are able, to reset their password by email. By default this enabled, when a mail server is configured. - partdb.users.enforce_unique_ipn: '%env(bool:ENFORCE_UNIQUE_IPN)%' # Config if users are able, to enter an already available IPN. In this case a unique increment is appended to the user input. ###################################################################################################################### # Mail settings diff --git a/config/services.yaml b/config/services.yaml index fa70e87c..f78f5209 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -231,28 +231,16 @@ services: tags: - { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' } - App\Controller\PartController: - bind: - $autocompletePartDigits: '%partdb.autocomplete_part_digits%' - - App\Controller\TypeaheadController: - bind: - $autocompletePartDigits: '%partdb.autocomplete_part_digits%' - App\Repository\PartRepository: arguments: $translator: '@translator' tags: ['doctrine.repository_service'] App\EventSubscriber\UserSystem\PartUniqueIpnSubscriber: - arguments: - $enforceUniqueIpn: '%partdb.users.enforce_unique_ipn%' tags: - { name: doctrine.event_subscriber } App\Validator\Constraints\UniquePartIpnValidator: - arguments: - $enforceUniqueIpn: '%partdb.users.enforce_unique_ipn%' tags: [ 'validator.constraint_validator' ] # We are needing this service inside a migration, where only the container is injected. So we need to define it as public, to access it from the container. diff --git a/docs/configuration.md b/docs/configuration.md index 0292242c..8f48940f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -116,7 +116,9 @@ 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 -* `AUTOCOMPLETE_PART_DIGITS`: Defines the fixed number of digits used as the increment at the end of an IPN (Internal Part Number). +* `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_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. @@ -132,8 +134,6 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept sent from. * `ALLOW_EMAIL_PW_RESET`: Set this value to true, if you want to allow users to reset their password via an email notification. You have to configure the mail provider first before via the MAILER_DSN setting. -* `ENFORCE_UNIQUE_IPN`: 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. ### Table related settings diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 0c7a24e3..6e9d8bc7 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -47,6 +47,7 @@ use App\Services\Parts\PartLotWithdrawAddHelper; use App\Services\Parts\PricedetailHelper; use App\Services\ProjectSystem\ProjectBuildPartHelper; use App\Settings\BehaviorSettings\PartInfoSettings; +use App\Settings\MiscSettings\IpnSuggestSettings; use DateTime; use Doctrine\ORM\EntityManagerInterface; use Exception; @@ -74,7 +75,7 @@ final class PartController extends AbstractController private readonly EntityManagerInterface $em, private readonly EventCommentHelper $commentHelper, private readonly PartInfoSettings $partInfoSettings, - private readonly int $autocompletePartDigits, + private readonly IpnSuggestSettings $ipnSuggestSettings, ) { } @@ -451,7 +452,7 @@ final class PartController extends AbstractController $template, [ 'part' => $new_part, - 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, base64_encode($data->getDescription()), $this->autocompletePartDigits), + 'ipnSuggestions' => $partRepository->autoCompleteIpn($data, base64_encode($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 f1e83d21..8262506d 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Controller; use App\Entity\Parameters\AbstractParameter; +use App\Settings\MiscSettings\IpnSuggestSettings; use Symfony\Component\HttpFoundation\Response; use App\Entity\Attachments\Attachment; use App\Entity\Parts\Category; @@ -63,7 +64,7 @@ class TypeaheadController extends AbstractController public function __construct( protected AttachmentURLGenerator $urlGenerator, protected Packages $assets, - protected int $autocompletePartDigits + protected IpnSuggestSettings $ipnSuggestSettings, ) { } @@ -207,7 +208,7 @@ class TypeaheadController extends AbstractController $clonedPart->setCategory($category); $partRepository = $entityManager->getRepository(Part::class); - $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, $this->autocompletePartDigits); + $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, $this->ipnSuggestSettings->suggestPartDigits); return new JsonResponse($ipnSuggestions); } diff --git a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php index 9cff3166..498a9e88 100644 --- a/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php +++ b/src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php @@ -3,6 +3,7 @@ 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; @@ -12,7 +13,7 @@ class PartUniqueIpnSubscriber implements EventSubscriber { public function __construct( private EntityManagerInterface $entityManager, - private readonly bool $enforceUniqueIpn = false + private IpnSuggestSettings $ipnSuggestSettings ) { } @@ -53,7 +54,7 @@ class PartUniqueIpnSubscriber implements EventSubscriber ->findOneBy(['ipn' => $part->getIpn()]); if ($existingPart && $existingPart->getId() !== $part->getId()) { - if ($this->enforceUniqueIpn) { + if ($this->ipnSuggestSettings->enableUniqueCheck) { return; } @@ -70,4 +71,4 @@ class PartUniqueIpnSubscriber implements EventSubscriber $part->setIpn($originalIpn . "_$increment"); } } -} \ No newline at end of file +} diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index 69361553..c6588731 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -118,13 +118,13 @@ class PartRepository extends NamedDBElementRepository * * @param Part $part The part for which autocomplete suggestions are generated. * @param string $description Base64-encoded description to assist in generating suggestions. - * @param int $autocompletePartDigits The number of digits used in autocomplete increments. + * @param int $suggestPartDigits The number of digits used in autocomplete increments. * * @return array An associative array containing the following keys: * - 'commonPrefixes': List of common prefixes found for the part. * - 'prefixesPartIncrement': Increments for the generated prefixes, including hierarchical prefixes. */ - public function autoCompleteIpn(Part $part, string $description, int $autocompletePartDigits): array + public function autoCompleteIpn(Part $part, string $description, int $suggestPartDigits): array { $category = $part->getCategory(); $ipnSuggestions = ['commonPrefixes' => [], 'prefixesPartIncrement' => []]; @@ -140,7 +140,7 @@ class PartRepository extends NamedDBElementRepository $directIpnPrefixEmpty = $category->getPartIpnPrefix() === ''; $currentPath = $currentPath === '' ? 'n.a.' : $currentPath; - $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $autocompletePartDigits); + $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $suggestPartDigits); $ipnSuggestions['commonPrefixes'][] = [ 'title' => $currentPath . '-', @@ -181,7 +181,7 @@ class PartRepository extends NamedDBElementRepository 'description' => $this->translator->trans('part.edit.tab.advanced.ipn.prefix.hierarchical.no_increment') ]; - $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $autocompletePartDigits); + $increment = $this->generateNextPossiblePartIncrement($currentPath, $part, $suggestPartDigits); $ipnSuggestions['prefixesPartIncrement'][] = [ 'title' => $currentPath . '-' . $increment, @@ -249,18 +249,18 @@ class PartRepository extends NamedDBElementRepository * * @param string $currentPath The base path or prefix for the part's identifier. * @param Part $currentPart The part entity for which the increment is being generated. - * @param int $autocompletePartDigits The number of digits reserved for the increment. + * @param int $suggestPartDigits The number of digits reserved for the increment. * * @return string|null The next possible increment as a zero-padded string, or null if it cannot be generated. * * @throws NonUniqueResultException If the query returns non-unique results. * @throws NoResultException If the query fails to return a result. */ - private function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $autocompletePartDigits): ?string + private function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $suggestPartDigits): ?string { $qb = $this->createQueryBuilder('part'); - $expectedLength = strlen($currentPath) + 1 + $autocompletePartDigits; // Path + '-' + $autocompletePartDigits digits + $expectedLength = strlen($currentPath) + 1 + $suggestPartDigits; // Path + '-' + $suggestPartDigits digits // Fetch all parts in the given category, sorted by their ID in ascending order $qb->select('part') @@ -281,14 +281,14 @@ class PartRepository extends NamedDBElementRepository if ($part->getId() === $currentPart->getId()) { // Extract and return the current part's increment directly - $incrementPart = substr($part->getIpn(), -$autocompletePartDigits); + $incrementPart = substr($part->getIpn(), -$suggestPartDigits); if (is_numeric($incrementPart)) { - return str_pad((string) $incrementPart, $autocompletePartDigits, '0', STR_PAD_LEFT); + return str_pad((string) $incrementPart, $suggestPartDigits, '0', STR_PAD_LEFT); } } // Extract last $autocompletePartDigits digits for possible available part increment - $incrementPart = substr($part->getIpn(), -$autocompletePartDigits); + $incrementPart = substr($part->getIpn(), -$suggestPartDigits); if (is_numeric($incrementPart)) { $usedIncrements[] = (int) $incrementPart; } @@ -302,7 +302,7 @@ class PartRepository extends NamedDBElementRepository $nextIncrement++; } - return str_pad((string) $nextIncrement, $autocompletePartDigits, '0', STR_PAD_LEFT); + return str_pad((string) $nextIncrement, $suggestPartDigits, '0', STR_PAD_LEFT); } /** diff --git a/src/Settings/MiscSettings/IpnSuggestSettings.php b/src/Settings/MiscSettings/IpnSuggestSettings.php new file mode 100644 index 00000000..1ef94b2f --- /dev/null +++ b/src/Settings/MiscSettings/IpnSuggestSettings.php @@ -0,0 +1,54 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\MiscSettings; + +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints as Assert; + +#[Settings(label: new TM("settings.misc.ipn_suggest"))] +#[SettingsIcon("fa-list")] +class IpnSuggestSettings +{ + use SettingsTrait; + + #[SettingsParameter( + label: new TM("settings.misc.ipn_suggest.enableUniqueCheck"), + envVar: "bool:IPN_ENABLE_UNIQUE_CHECK", envVarMode: EnvVarMode::OVERWRITE, + )] + public bool $enableUniqueCheck = true; + + #[SettingsParameter(label: new TM("settings.misc.ipn_suggest.suggestPartDigits"), + description: new TM("settings.misc.ipn_suggest.suggestPartDigits.help"), + formOptions: ['attr' => ['min' => 1, 'max' => 100]], + envVar: "int:IPN_SUGGEST_PART_DIGITS", envVarMode: EnvVarMode::OVERWRITE + )] + #[Assert\Range(min: 1, max: 6)] + public int $suggestPartDigits = 4; +} diff --git a/src/Settings/MiscSettings/MiscSettings.php b/src/Settings/MiscSettings/MiscSettings.php index b8a3a73f..fa6a7349 100644 --- a/src/Settings/MiscSettings/MiscSettings.php +++ b/src/Settings/MiscSettings/MiscSettings.php @@ -34,4 +34,7 @@ class MiscSettings #[EmbeddedSettings] public ?ExchangeRateSettings $exchangeRate = null; -} \ No newline at end of file + + #[EmbeddedSettings] + public ?IpnSuggestSettings $ipnSuggestSettings = null; +} diff --git a/src/Validator/Constraints/UniquePartIpnValidator.php b/src/Validator/Constraints/UniquePartIpnValidator.php index 019202f8..641ffe47 100644 --- a/src/Validator/Constraints/UniquePartIpnValidator.php +++ b/src/Validator/Constraints/UniquePartIpnValidator.php @@ -3,6 +3,7 @@ namespace App\Validator\Constraints; use App\Entity\Parts\Part; +use App\Settings\MiscSettings\IpnSuggestSettings; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Doctrine\ORM\EntityManagerInterface; @@ -10,12 +11,12 @@ use Doctrine\ORM\EntityManagerInterface; class UniquePartIpnValidator extends ConstraintValidator { private EntityManagerInterface $entityManager; - private bool $enforceUniqueIpn; + private IpnSuggestSettings $ipnSuggestSettings; - public function __construct(EntityManagerInterface $entityManager, bool $enforceUniqueIpn) + public function __construct(EntityManagerInterface $entityManager, IpnSuggestSettings $ipnSuggestSettings) { $this->entityManager = $entityManager; - $this->enforceUniqueIpn = $enforceUniqueIpn; + $this->ipnSuggestSettings = $ipnSuggestSettings; } public function validate($value, Constraint $constraint) @@ -24,7 +25,7 @@ class UniquePartIpnValidator extends ConstraintValidator return; } - if (!$this->enforceUniqueIpn) { + if (!$this->ipnSuggestSettings->enableUniqueCheck) { return; } @@ -40,12 +41,10 @@ class UniquePartIpnValidator extends ConstraintValidator foreach ($existingParts as $existingPart) { if ($currentPart->getId() !== $existingPart->getId()) { - if ($this->enforceUniqueIpn) { - $this->context->buildViolation($constraint->message) - ->setParameter('{{ value }}', $value) - ->addViolation(); - } + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $value) + ->addViolation(); } } } -} \ No newline at end of file +} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index b7d5c3e9..b8d24325 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -13053,6 +13053,30 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Pokud potřebujete směnné kurzy mezi měnami mimo eurozónu, můžete zde zadat API klíč z fixer.io. + + + settings.misc.ipn_suggest + 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.suggestPartDigits + Počet čísel pro inkrement + + + + + settings.misc.ipn_suggest.suggestPartDigits.help + Počet číslic použitých pro inkrementální číslování součástí v návrhovém systému IPN (Interní číslo součástky). + + settings.behavior.part_info diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index a726c3cd..3ea13fdc 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -13133,6 +13133,30 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Wenn Sie Wechselkurse zwischen Nicht-Euro-Währungen benötigen, können Sie hier einen API-Schlüssel von fixer.io eingeben. + + + settings.misc.ipn_suggest + 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.suggestPartDigits + Stellen für numerisches Inkrement + + + + + settings.misc.ipn_suggest.suggestPartDigits.help + Die Anzahl der Ziffern, die für die inkrementale Nummerierung von Teilen im IPN-Vorschlagssystem verwendet werden. + + settings.behavior.part_info diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index a231458b..82dad84f 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -13134,6 +13134,30 @@ Please note, that you can not impersonate a disabled user. If you try you will g If you need exchange rates between non-euro currencies, you can input an API key from fixer.io here. + + + settings.misc.ipn_suggest + 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.suggestPartDigits + Increment Digits + + + + + settings.misc.ipn_suggest.suggestPartDigits.help + The number of digits used for the incremental numbering of parts in the IPN (Internal Part Number) suggestion system. + + settings.behavior.part_info From 9b90a513c92428bbcaf7870862562b1d72a082c5 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Fri, 26 Sep 2025 15:08:42 +0200 Subject: [PATCH 09/10] Anpassungen aus phpstan Analyse --- src/Controller/TypeaheadController.php | 1 + src/Repository/PartRepository.php | 8 ++++---- src/Validator/Constraints/UniquePartIpnValidator.php | 5 +++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 8262506d..fe4f029f 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -202,6 +202,7 @@ class TypeaheadController extends AbstractController /** @var Part $part */ $part = $partId !== null ? $entityManager->getRepository(Part::class)->find($partId) : new Part(); + /** @var Category|null $category */ $category = $entityManager->getRepository(Category::class)->find($categoryId); $clonedPart = clone $part; diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index c6588731..23318635 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -128,7 +128,7 @@ class PartRepository extends NamedDBElementRepository { $category = $part->getCategory(); $ipnSuggestions = ['commonPrefixes' => [], 'prefixesPartIncrement' => []]; - $description = base64_decode($description); + $description = base64_decode($description, true); if (strlen($description) > 150) { $description = substr($description, 0, 150); @@ -251,12 +251,12 @@ class PartRepository extends NamedDBElementRepository * @param Part $currentPart The part entity for which the increment is being generated. * @param int $suggestPartDigits The number of digits reserved for the increment. * - * @return string|null The next possible increment as a zero-padded string, or null if it cannot be generated. + * @return string The next possible increment as a zero-padded string. * * @throws NonUniqueResultException If the query returns non-unique results. * @throws NoResultException If the query fails to return a result. */ - private function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $suggestPartDigits): ?string + private function generateNextPossiblePartIncrement(string $currentPath, Part $currentPart, int $suggestPartDigits): string { $qb = $this->createQueryBuilder('part'); @@ -298,7 +298,7 @@ class PartRepository extends NamedDBElementRepository // Generate the next free $autocompletePartDigits-digit increment $nextIncrement = 1; // Start at the beginning - while (in_array($nextIncrement, $usedIncrements)) { + while (in_array($nextIncrement, $usedIncrements, true)) { $nextIncrement++; } diff --git a/src/Validator/Constraints/UniquePartIpnValidator.php b/src/Validator/Constraints/UniquePartIpnValidator.php index 641ffe47..edee1190 100644 --- a/src/Validator/Constraints/UniquePartIpnValidator.php +++ b/src/Validator/Constraints/UniquePartIpnValidator.php @@ -29,6 +29,11 @@ class UniquePartIpnValidator extends ConstraintValidator return; } + // Stelle sicher, dass es unser eigenes Constraint ist (wichtig für PHPStan) + if (!$constraint instanceof UniquePartIpnConstraint) { + return; + } + /** @var Part $currentPart */ $currentPart = $this->context->getObject(); From 654c2ed2af8422eaa68923dcff5ba978e0dac856 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Mon, 29 Sep 2025 13:54:13 +0200 Subject: [PATCH 10/10] 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