mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-11 20:59:36 +00:00
IPN-Vorschlagslogik erweitert und Bauteil-IPN vereindeutigt
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.
This commit is contained in:
parent
9b90a513c9
commit
654c2ed2af
16 changed files with 165 additions and 117 deletions
|
|
@ -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));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' ]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' => [
|
||||
|
|
|
|||
|
|
@ -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 "_<Zahl>"
|
||||
$baseIpn = preg_replace('/_\d+$/', '', $baseIpn); // Remove existing "_ <number>"
|
||||
|
||||
if ($baseIpn === '') {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class UniquePartIpnValidator extends ConstraintValidator
|
|||
return;
|
||||
}
|
||||
|
||||
if (!$this->ipnSuggestSettings->enableUniqueCheck) {
|
||||
if ($this->ipnSuggestSettings->autoAppendSuffix) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13059,10 +13059,10 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
|
|||
<target>Seznam návrhů IPN součástek</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kdi8mT4" name="settings.misc.ipn_suggest.enableUniqueCheck">
|
||||
<unit id="kdi8mT4" name="settings.misc.ipn_suggest.autoAppendSuffix">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.enableUniqueCheck</source>
|
||||
<target>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.</target>
|
||||
<source>settings.misc.ipn_suggest.autoAppendSuffix</source>
|
||||
<target>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.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="rociEg6" name="settings.misc.ipn_suggest.suggestPartDigits">
|
||||
|
|
@ -13071,6 +13071,12 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
|
|||
<target>Počet čísel pro inkrement</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jdz6B4c" name="settings.misc.ipn_suggest.useDuplicateDescription">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.useDuplicateDescription</source>
|
||||
<target>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ů.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="judfiK3" name="settings.misc.ipn_suggest.suggestPartDigits.help">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.suggestPartDigits.help</source>
|
||||
|
|
|
|||
|
|
@ -13139,10 +13139,10 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
|||
<target>Bauteil IPN-Vorschlagsliste</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kdi8mT4" name="settings.misc.ipn_suggest.enableUniqueCheck">
|
||||
<unit id="kdi8mT4" name="settings.misc.ipn_suggest.autoAppendSuffix">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.enableUniqueCheck</source>
|
||||
<target>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.</target>
|
||||
<source>settings.misc.ipn_suggest.autoAppendSuffix</source>
|
||||
<target>Wenn diese Option aktiviert ist, wird der Eingabe ein inkrementelles Suffix hinzugefügt, wenn eine vorhandene IPN beim Speichern erneut eingegeben wird.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="rociEg6" name="settings.misc.ipn_suggest.suggestPartDigits">
|
||||
|
|
@ -13151,6 +13151,12 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
|||
<target>Stellen für numerisches Inkrement</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jdz6B4c" name="settings.misc.ipn_suggest.useDuplicateDescription">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.useDuplicateDescription</source>
|
||||
<target>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.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="judfiK3" name="settings.misc.ipn_suggest.suggestPartDigits.help">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.suggestPartDigits.help</source>
|
||||
|
|
|
|||
|
|
@ -13140,10 +13140,10 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>Part IPN Suggest</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kdi8mT4" name="settings.misc.ipn_suggest.enableUniqueCheck">
|
||||
<unit id="kdi8mT4" name="settings.misc.ipn_suggest.autoAppendSuffix">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.enableUniqueCheck</source>
|
||||
<target>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.</target>
|
||||
<source>settings.misc.ipn_suggest.autoAppendSuffix</source>
|
||||
<target>Do you want an incremental number to be added to the user input when entering an existing IPN again upon saving?</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="rociEg6" name="settings.misc.ipn_suggest.suggestPartDigits">
|
||||
|
|
@ -13152,6 +13152,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>Increment Digits</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jdz6B4c" name="settings.misc.ipn_suggest.useDuplicateDescription">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.useDuplicateDescription</source>
|
||||
<target>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.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="judfiK3" name="settings.misc.ipn_suggest.suggestPartDigits.help">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.suggestPartDigits.help</source>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue