mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-12 05:09:33 +00:00
Merge remote-tracking branch 'origin/feature/automatic-ipn-generation' into feature/all-features
# Conflicts: # assets/controllers/elements/ipn_suggestion_controller.js # config/services.yaml # docs/configuration.md # migrations/Version20250325073036.php # src/Controller/PartController.php # src/Controller/TypeaheadController.php # src/EventSubscriber/UserSystem/PartUniqueIpnSubscriber.php # src/Form/Part/PartBaseType.php # src/Repository/PartRepository.php # src/Settings/MiscSettings/MiscSettings.php # src/Validator/Constraints/UniquePartIpnConstraint.php # src/Validator/Constraints/UniquePartIpnValidator.php # templates/parts/edit/_advanced.html.twig # translations/messages.cs.xlf # translations/messages.de.xlf # translations/messages.el.xlf # translations/messages.en.xlf # translations/messages.fr.xlf # translations/messages.ja.xlf # translations/messages.nl.xlf
This commit is contained in:
commit
7a5885839e
16 changed files with 323 additions and 236 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));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,28 +233,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 }
|
||||
- { name: doctrine.event_listener, event: onFlush, connection: default }
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -116,10 +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
|
||||
* `AUTOCOMPLETE_PART_DIGITS`: Defines the fixed number of digits used as the increment at the end of an IPN (Internal Part Number).
|
||||
* `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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ use App\Services\Parts\PricedetailHelper;
|
|||
use App\Services\AssemblySystem\AssemblyBuildPartHelper;
|
||||
use App\Services\ProjectSystem\ProjectBuildPartHelper;
|
||||
use App\Settings\BehaviorSettings\PartInfoSettings;
|
||||
use App\Settings\MiscSettings\IpnSuggestSettings;
|
||||
use DateTime;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
|
|
@ -76,7 +77,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,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -464,7 +465,7 @@ final class PartController extends AbstractController
|
|||
$template,
|
||||
[
|
||||
'part' => $new_part,
|
||||
'ipnSuggestions' => $partRepository->autoCompleteIpn($data, base64_encode($data->getDescription()), $this->autocompletePartDigits),
|
||||
'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,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ namespace App\Controller;
|
|||
|
||||
use App\Entity\AssemblySystem\Assembly;
|
||||
use App\Entity\Parameters\AbstractParameter;
|
||||
use App\Settings\MiscSettings\IpnSuggestSettings;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Services\Attachments\AssemblyPreviewGenerator;
|
||||
use App\Services\Attachments\ProjectPreviewGenerator;
|
||||
|
|
@ -57,8 +58,6 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* In this controller the endpoints for the typeaheads are collected.
|
||||
|
|
@ -69,8 +68,7 @@ class TypeaheadController extends AbstractController
|
|||
public function __construct(
|
||||
protected AttachmentURLGenerator $urlGenerator,
|
||||
protected Packages $assets,
|
||||
protected TranslatorInterface $translator,
|
||||
protected int $autocompletePartDigits,
|
||||
protected IpnSuggestSettings $ipnSuggestSettings,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -119,22 +117,19 @@ class TypeaheadController extends AbstractController
|
|||
'group' => GroupParameter::class,
|
||||
'measurement_unit' => MeasurementUnitParameter::class,
|
||||
'currency' => Currency::class,
|
||||
default => throw new InvalidArgumentException('Invalid parameter type: '.$type),
|
||||
default => throw new \InvalidArgumentException('Invalid parameter type: '.$type),
|
||||
};
|
||||
}
|
||||
|
||||
#[Route(path: '/parts/search/{query}', name: 'typeahead_parts')]
|
||||
public function parts(
|
||||
EntityManagerInterface $entityManager,
|
||||
PartPreviewGenerator $previewGenerator,
|
||||
AttachmentURLGenerator $attachmentURLGenerator,
|
||||
string $query = ""
|
||||
): JsonResponse {
|
||||
public function parts(EntityManagerInterface $entityManager, PartPreviewGenerator $previewGenerator,
|
||||
AttachmentURLGenerator $attachmentURLGenerator, string $query = ""): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('@parts.read');
|
||||
|
||||
$partRepository = $entityManager->getRepository(Part::class);
|
||||
$repo = $entityManager->getRepository(Part::class);
|
||||
|
||||
$parts = $partRepository->autocompleteSearch($query, 100);
|
||||
$parts = $repo->autocompleteSearch($query, 100);
|
||||
|
||||
$data = [];
|
||||
foreach ($parts as $part) {
|
||||
|
|
@ -154,88 +149,12 @@ class TypeaheadController extends AbstractController
|
|||
'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '',
|
||||
'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'),
|
||||
'image' => $preview_url,
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse($data);
|
||||
}
|
||||
|
||||
#[Route(path: '/projects/search/{query}', name: 'typeahead_projects')]
|
||||
public function projects(
|
||||
EntityManagerInterface $entityManager,
|
||||
ProjectPreviewGenerator $projectPreviewGenerator,
|
||||
AttachmentURLGenerator $attachmentURLGenerator,
|
||||
string $query = ""
|
||||
): JsonResponse {
|
||||
$this->denyAccessUnlessGranted('@projects.read');
|
||||
|
||||
$result = [];
|
||||
|
||||
$projectRepository = $entityManager->getRepository(Project::class);
|
||||
|
||||
$projects = $projectRepository->autocompleteSearch($query, 100);
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$preview_attachment = $projectPreviewGenerator->getTablePreviewAttachment($project);
|
||||
|
||||
if($preview_attachment instanceof Attachment) {
|
||||
$preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm');
|
||||
} else {
|
||||
$preview_url = '';
|
||||
}
|
||||
|
||||
/** @var Project $project */
|
||||
$result[] = [
|
||||
'id' => $project->getID(),
|
||||
'name' => $project->getName(),
|
||||
'category' => '',
|
||||
'footprint' => '',
|
||||
'description' => mb_strimwidth($project->getDescription(), 0, 127, '...'),
|
||||
'image' => $preview_url,
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse($result);
|
||||
}
|
||||
|
||||
#[Route(path: '/assemblies/search/{query}', name: 'typeahead_assemblies')]
|
||||
public function assemblies(
|
||||
EntityManagerInterface $entityManager,
|
||||
AssemblyPreviewGenerator $assemblyPreviewGenerator,
|
||||
AttachmentURLGenerator $attachmentURLGenerator,
|
||||
string $query = ""
|
||||
): JsonResponse {
|
||||
$this->denyAccessUnlessGranted('@assemblies.read');
|
||||
|
||||
$result = [];
|
||||
|
||||
$assemblyRepository = $entityManager->getRepository(Assembly::class);
|
||||
|
||||
$assemblies = $assemblyRepository->autocompleteSearch($query, 100);
|
||||
|
||||
foreach ($assemblies as $assembly) {
|
||||
$preview_attachment = $assemblyPreviewGenerator->getTablePreviewAttachment($assembly);
|
||||
|
||||
if($preview_attachment instanceof Attachment) {
|
||||
$preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm');
|
||||
} else {
|
||||
$preview_url = '';
|
||||
}
|
||||
|
||||
/** @var Assembly $assembly */
|
||||
$result[] = [
|
||||
'id' => $assembly->getID(),
|
||||
'name' => $assembly->getName(),
|
||||
'category' => '',
|
||||
'footprint' => '',
|
||||
'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'),
|
||||
'image' => $preview_url,
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse($result);
|
||||
}
|
||||
|
||||
#[Route(path: '/parameters/{type}/search/{query}', name: 'typeahead_parameters', requirements: ['type' => '.+'])]
|
||||
public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse
|
||||
{
|
||||
|
|
@ -283,17 +202,18 @@ 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();
|
||||
/** @var Category|null $category */
|
||||
$category = $entityManager->getRepository(Category::class)->find($categoryId);
|
||||
|
||||
$clonedPart = clone $part;
|
||||
$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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,71 +3,95 @@
|
|||
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 readonly bool $enforceUniqueIpn = false
|
||||
private IpnSuggestSettings $ipnSuggestSettings
|
||||
) {
|
||||
}
|
||||
|
||||
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->enforceUniqueIpn) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,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;
|
||||
|
|
@ -57,8 +58,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
|
||||
|
|
@ -70,6 +75,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, [
|
||||
|
|
@ -84,11 +99,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,35 +120,24 @@ 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 int $autocompletePartDigits The number of digits used in autocomplete increments.
|
||||
* @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:
|
||||
* - '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' => []];
|
||||
$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) {
|
||||
$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')
|
||||
];
|
||||
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,
|
||||
|
|
@ -181,7 +187,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 +255,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.
|
||||
* @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 $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 +287,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;
|
||||
}
|
||||
|
|
@ -298,11 +304,11 @@ 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++;
|
||||
}
|
||||
|
||||
return str_pad((string) $nextIncrement, $autocompletePartDigits, '0', STR_PAD_LEFT);
|
||||
return str_pad((string) $nextIncrement, $suggestPartDigits, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
60
src/Settings/MiscSettings/IpnSuggestSettings.php
Normal file
60
src/Settings/MiscSettings/IpnSuggestSettings.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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.autoAppendSuffix"),
|
||||
envVar: "bool:IPN_AUTO_APPEND_SUFFIX", envVarMode: EnvVarMode::OVERWRITE,
|
||||
)]
|
||||
public bool $autoAppendSuffix = 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;
|
||||
|
||||
#[SettingsParameter(
|
||||
label: new TM("settings.misc.ipn_suggest.useDuplicateDescription"),
|
||||
envVar: "bool:IPN_USE_DUPLICATE_DESCRIPTION", envVarMode: EnvVarMode::OVERWRITE,
|
||||
)]
|
||||
public bool $useDuplicateDescription = false;
|
||||
}
|
||||
|
|
@ -37,4 +37,7 @@ class MiscSettings
|
|||
|
||||
#[EmbeddedSettings]
|
||||
public ?AssemblySettings $assembly = null;
|
||||
|
||||
#[EmbeddedSettings]
|
||||
public ?IpnSuggestSettings $ipnSuggestSettings = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,12 @@ class UniquePartIpnValidator extends ConstraintValidator
|
|||
return;
|
||||
}
|
||||
|
||||
if (!$this->enforceUniqueIpn) {
|
||||
if ($this->ipnSuggestSettings->autoAppendSuffix) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stelle sicher, dass es unser eigenes Constraint ist (wichtig für PHPStan)
|
||||
if (!$constraint instanceof UniquePartIpnConstraint) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -40,12 +46,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4825,7 +4825,7 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn
|
|||
<target>Název</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="eshqdG." name="part.table.id">
|
||||
<unit id="rW_SFJE" name="part.table.id">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">Part-DB1\src\DataTables\PartsDataTable.php:178</note>
|
||||
<note priority="1">Part-DB1\src\DataTables\PartsDataTable.php:126</note>
|
||||
|
|
@ -13528,6 +13528,36 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
|
|||
<target>Použít zástupný symbol %%ipn%% v názvu sestavy. Zástupný symbol bude při ukládání nahrazen vstupem IPN.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jd7tEu3" name="settings.misc.ipn_suggest">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest</source>
|
||||
<target>Seznam návrhů IPN součástek</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kdi8mT4" name="settings.misc.ipn_suggest.autoAppendSuffix">
|
||||
<segment state="translated">
|
||||
<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">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.suggestPartDigits</source>
|
||||
<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>
|
||||
<target>Počet číslic použitých pro inkrementální číslování součástí v návrhovém systému IPN (Interní číslo součástky).</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Ffr5xYM" name="settings.behavior.part_info">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.part_info</source>
|
||||
|
|
|
|||
|
|
@ -14411,6 +14411,36 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
|||
<target>Verwenden Sie einen %%ipn%%-Platzhalter im Namen einer Baugruppe. Der Platzhalter wird beim Speichern durch die eingegebene IPN ersetzt.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jd7tEu3" name="settings.misc.ipn_suggest">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest</source>
|
||||
<target>Bauteil IPN-Vorschlagsliste</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kdi8mT4" name="settings.misc.ipn_suggest.autoAppendSuffix">
|
||||
<segment state="translated">
|
||||
<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">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.suggestPartDigits</source>
|
||||
<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>
|
||||
<target>Die Anzahl der Ziffern, die für die inkrementale Nummerierung von Teilen im IPN-Vorschlagssystem verwendet werden.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Ffr5xYM" name="settings.behavior.part_info">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.part_info</source>
|
||||
|
|
|
|||
|
|
@ -5803,12 +5803,6 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
|
|||
<target>Measuring unit</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="G1hmQdb" name="part.edit.partCustomState">
|
||||
<segment state="translated">
|
||||
<source>part.edit.partCustomState</source>
|
||||
<target>Custom part state</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="oY_9HE9" name="part.edit.comment">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">Part-DB1\src\Form\Part\PartBaseType.php:212</note>
|
||||
|
|
@ -14412,6 +14406,36 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>Use an %%ipn%% placeholder in the name of an assembly. Placeholder is replaced with the ipn input while saving.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jd7tEu3" name="settings.misc.ipn_suggest">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest</source>
|
||||
<target>Part IPN Suggest</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kdi8mT4" name="settings.misc.ipn_suggest.autoAppendSuffix">
|
||||
<segment state="translated">
|
||||
<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">
|
||||
<segment state="translated">
|
||||
<source>settings.misc.ipn_suggest.suggestPartDigits</source>
|
||||
<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>
|
||||
<target>The number of digits used for the incremental numbering of parts in the IPN (Internal Part Number) suggestion system.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="Ffr5xYM" name="settings.behavior.part_info">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.part_info</source>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue