diff --git a/assets/controllers/elements/collection_type_controller.js b/assets/controllers/elements/collection_type_controller.js index 048600a9..647ed5e5 100644 --- a/assets/controllers/elements/collection_type_controller.js +++ b/assets/controllers/elements/collection_type_controller.js @@ -81,7 +81,7 @@ export default class extends Controller { //Afterwards return the newly created row if(targetTable.tBodies[0]) { targetTable.tBodies[0].insertAdjacentHTML('beforeend', newElementStr); - ret = targetTable.tBodies[0].lastElementChild; + ret = targetTable.tBodies[0].lastElementChild; } else { //Otherwise just insert it targetTable.insertAdjacentHTML('beforeend', newElementStr); ret = targetTable.lastElementChild; @@ -90,10 +90,20 @@ export default class extends Controller { //Trigger an event to notify other components that a new element has been created, so they can for example initialize select2 on it targetTable.dispatchEvent(new CustomEvent("collection:elementAdded", {bubbles: true})); + this.focusNumberInput(ret); + return ret; } + focusNumberInput(element) { + const fields = element.querySelectorAll("input[type=number]"); + //Focus the first available number input field to open the numeric keyboard on mobile devices + if(fields.length > 0) { + fields[0].focus(); + } + } + /** * This action opens a file dialog to select multiple files and then creates a new element for each file, where * the file is assigned to the input field. diff --git a/assets/controllers/pages/part_withdraw_modal_controller.js b/assets/controllers/pages/part_withdraw_modal_controller.js index 2d6742b4..0e5c0fc5 100644 --- a/assets/controllers/pages/part_withdraw_modal_controller.js +++ b/assets/controllers/pages/part_withdraw_modal_controller.js @@ -5,6 +5,7 @@ export default class extends Controller { connect() { this.element.addEventListener('show.bs.modal', event => this._handleModalOpen(event)); + this.element.addEventListener('shown.bs.modal', event => this._handleModalShown(event)); } _handleModalOpen(event) { @@ -61,4 +62,8 @@ export default class extends Controller amountInput.setAttribute('max', lotAmount); } } + + _handleModalShown(event) { + this.element.querySelector('input[name="amount"]').focus(); + } } \ No newline at end of file diff --git a/composer.lock b/composer.lock index 6566ebbc..2db828a5 100644 --- a/composer.lock +++ b/composer.lock @@ -16558,16 +16558,16 @@ }, { "name": "thecodingmachine/safe", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236" + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236", - "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", "shasum": "" }, "require": { @@ -16677,7 +16677,7 @@ "description": "PHP core functions that throw exceptions instead of returning FALSE on error", "support": { "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v3.3.0" + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" }, "funding": [ { @@ -16688,12 +16688,16 @@ "url": "https://github.com/shish", "type": "github" }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, { "url": "https://github.com/staabm", "type": "github" } ], - "time": "2025-05-14T06:15:44+00:00" + "time": "2026-02-04T18:08:13+00:00" }, { "name": "tiendanube/gtinvalidation", diff --git a/src/ApiResource/LabelGenerationRequest.php b/src/ApiResource/LabelGenerationRequest.php new file mode 100644 index 00000000..9b4462a0 --- /dev/null +++ b/src/ApiResource/LabelGenerationRequest.php @@ -0,0 +1,84 @@ +. + */ + +namespace App\ApiResource; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Post; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\OpenApi\Model\RequestBody; +use ApiPlatform\OpenApi\Model\Response; +use App\Entity\LabelSystem\LabelSupportedElement; +use App\State\LabelGenerationProcessor; +use App\Validator\Constraints\Misc\ValidRange; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * API Resource for generating PDF labels for parts, part lots, or storage locations. + * This endpoint allows generating labels using saved label profiles. + */ +#[ApiResource( + uriTemplate: '/labels/generate', + description: 'Generate PDF labels for parts, part lots, or storage locations using label profiles.', + operations: [ + new Post( + inputFormats: ['json' => ['application/json']], + outputFormats: [], + openapi: new Operation( + responses: [ + "200" => new Response(description: "PDF file containing the generated labels"), + ], + summary: 'Generate PDF labels', + description: 'Generate PDF labels for one or more elements using a label profile. Returns a PDF file.', + requestBody: new RequestBody( + description: 'Label generation request', + required: true, + ), + ), + ) + ], + processor: LabelGenerationProcessor::class, +)] +class LabelGenerationRequest +{ + /** + * @var int The ID of the label profile to use for generation + */ + #[Assert\NotBlank(message: 'Profile ID is required')] + #[Assert\Positive(message: 'Profile ID must be a positive integer')] + public int $profileId = 0; + + /** + * @var string Comma-separated list of element IDs or ranges (e.g., "1,2,5-10,15") + */ + #[Assert\NotBlank(message: 'Element IDs are required')] + #[ValidRange()] + #[ApiProperty(example: "1,2,5-10,15")] + public string $elementIds = ''; + + /** + * @var LabelSupportedElement|null Optional: Override the element type. If not provided, uses profile's default. + */ + public ?LabelSupportedElement $elementType = null; +} diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index fbc5211d..d2faba76 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -47,6 +47,7 @@ use App\Services\EntityURLGenerator; use App\Services\Formatters\AmountFormatter; use App\Settings\BehaviorSettings\TableSettings; use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; use Omines\DataTablesBundle\Column\TextColumn; @@ -333,6 +334,7 @@ final class PartsDataTable implements DataTableTypeInterface ->addSelect('orderdetails') ->addSelect('attachments') ->addSelect('storelocations') + ->addSelect('projectBomEntries') ->from(Part::class, 'part') ->leftJoin('part.category', 'category') ->leftJoin('part.master_picture_attachment', 'master_picture_attachment') @@ -347,6 +349,7 @@ final class PartsDataTable implements DataTableTypeInterface ->leftJoin('part.partUnit', 'partUnit') ->leftJoin('part.partCustomState', 'partCustomState') ->leftJoin('part.parameters', 'parameters') + ->leftJoin('part.project_bom_entries', 'projectBomEntries') ->where('part.id IN (:ids)') ->setParameter('ids', $ids) @@ -364,7 +367,12 @@ final class PartsDataTable implements DataTableTypeInterface ->addGroupBy('attachments') ->addGroupBy('partUnit') ->addGroupBy('partCustomState') - ->addGroupBy('parameters'); + ->addGroupBy('parameters') + ->addGroupBy('projectBomEntries') + + ->setHint(Query::HINT_READ_ONLY, true) + ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, false) + ; //Get the results in the same order as the IDs were passed FieldHelper::addOrderByFieldParam($builder, 'part.id', 'ids'); diff --git a/src/Entity/LabelSystem/LabelProfile.php b/src/Entity/LabelSystem/LabelProfile.php index d3616c34..236c07f7 100644 --- a/src/Entity/LabelSystem/LabelProfile.php +++ b/src/Entity/LabelSystem/LabelProfile.php @@ -41,6 +41,12 @@ declare(strict_types=1); namespace App\Entity\LabelSystem; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\OpenApi\Model\Operation; use Doctrine\Common\Collections\Criteria; use App\Entity\Attachments\Attachment; use App\Repository\LabelProfileRepository; @@ -58,6 +64,22 @@ use Symfony\Component\Validator\Constraints as Assert; /** * @extends AttachmentContainingDBElement */ +#[ApiResource( + operations: [ + new Get( + normalizationContext: ['groups' => ['label_profile:read', 'simple']], + security: "is_granted('read', object)", + openapi: new Operation(summary: 'Get a label profile by ID') + ), + new GetCollection( + normalizationContext: ['groups' => ['label_profile:read', 'simple']], + security: "is_granted('@labels.create_labels')", + openapi: new Operation(summary: 'List all available label profiles') + ), + ], + paginationEnabled: false, +)] +#[ApiFilter(SearchFilter::class, properties: ['options.supported_element' => 'exact', 'show_in_dropdown' => 'exact'])] #[UniqueEntity(['name', 'options.supported_element'])] #[ORM\Entity(repositoryClass: LabelProfileRepository::class)] #[ORM\EntityListeners([TreeCacheInvalidationListener::class])] @@ -80,20 +102,21 @@ class LabelProfile extends AttachmentContainingDBElement */ #[Assert\Valid] #[ORM\Embedded(class: 'LabelOptions')] - #[Groups(["extended", "full", "import"])] + #[Groups(["extended", "full", "import", "label_profile:read"])] protected LabelOptions $options; /** * @var string The comment info for this element */ #[ORM\Column(type: Types::TEXT)] + #[Groups(["extended", "full", "import", "label_profile:read"])] protected string $comment = ''; /** * @var bool determines, if this label profile should be shown in the dropdown quick menu */ #[ORM\Column(type: Types::BOOLEAN)] - #[Groups(["extended", "full", "import"])] + #[Groups(["extended", "full", "import", "label_profile:read"])] protected bool $show_in_dropdown = true; public function __construct() diff --git a/src/Form/AdminPages/BaseEntityAdminForm.php b/src/Form/AdminPages/BaseEntityAdminForm.php index 5a4ef5bc..f4bf37f8 100644 --- a/src/Form/AdminPages/BaseEntityAdminForm.php +++ b/src/Form/AdminPages/BaseEntityAdminForm.php @@ -71,6 +71,7 @@ class BaseEntityAdminForm extends AbstractType 'label' => 'name.label', 'attr' => [ 'placeholder' => 'part.name.placeholder', + 'autofocus' => $is_new, ], 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); diff --git a/src/Form/Part/PartBaseType.php b/src/Form/Part/PartBaseType.php index 2145db93..6b929486 100644 --- a/src/Form/Part/PartBaseType.php +++ b/src/Form/Part/PartBaseType.php @@ -117,6 +117,7 @@ class PartBaseType extends AbstractType 'label' => 'part.edit.name', 'attr' => [ 'placeholder' => 'part.edit.name.placeholder', + 'autofocus' => $new_part, ], ]) ->add('description', RichTextEditorType::class, [ diff --git a/src/Services/InfoProviderSystem/PartInfoRetriever.php b/src/Services/InfoProviderSystem/PartInfoRetriever.php index 0eb74642..9a24f3ae 100644 --- a/src/Services/InfoProviderSystem/PartInfoRetriever.php +++ b/src/Services/InfoProviderSystem/PartInfoRetriever.php @@ -82,7 +82,7 @@ final class PartInfoRetriever protected function searchInProvider(InfoProviderInterface $provider, string $keyword): array { //Generate key and escape reserved characters from the provider id - $escaped_keyword = urlencode($keyword); + $escaped_keyword = hash('xxh3', $keyword); return $this->partInfoCache->get("search_{$provider->getProviderKey()}_{$escaped_keyword}", function (ItemInterface $item) use ($provider, $keyword) { //Set the expiration time $item->expiresAfter(!$this->debugMode ? self::CACHE_RESULT_EXPIRATION : 1); @@ -108,7 +108,7 @@ final class PartInfoRetriever } //Generate key and escape reserved characters from the provider id - $escaped_part_id = urlencode($part_id); + $escaped_part_id = hash('xxh3', $part_id); return $this->partInfoCache->get("details_{$provider_key}_{$escaped_part_id}", function (ItemInterface $item) use ($provider, $part_id) { //Set the expiration time $item->expiresAfter(!$this->debugMode ? self::CACHE_DETAIL_EXPIRATION : 1); @@ -145,4 +145,4 @@ final class PartInfoRetriever return $this->dto_to_entity_converter->convertPart($details); } -} \ No newline at end of file +} diff --git a/src/State/LabelGenerationProcessor.php b/src/State/LabelGenerationProcessor.php new file mode 100644 index 00000000..0472bbbd --- /dev/null +++ b/src/State/LabelGenerationProcessor.php @@ -0,0 +1,141 @@ +. + */ + +namespace App\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use App\ApiResource\LabelGenerationRequest; +use App\Entity\Base\AbstractDBElement; +use App\Entity\LabelSystem\LabelProfile; +use App\Repository\DBElementRepository; +use App\Repository\LabelProfileRepository; +use App\Services\ElementTypeNameGenerator; +use App\Services\LabelSystem\LabelGenerator; +use App\Services\Misc\RangeParser; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +class LabelGenerationProcessor implements ProcessorInterface +{ + public function __construct( + private readonly EntityManagerInterface $entityManager, + private readonly LabelGenerator $labelGenerator, + private readonly RangeParser $rangeParser, + private readonly ElementTypeNameGenerator $elementTypeNameGenerator, + private readonly Security $security, + ) { + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Response + { + // Check if user has permission to create labels + if (!$this->security->isGranted('@labels.create_labels')) { + throw new AccessDeniedHttpException('You do not have permission to generate labels.'); + } + + if (!$data instanceof LabelGenerationRequest) { + throw new BadRequestHttpException('Invalid request data for label generation.'); + } + + /** @var LabelGenerationRequest $request */ + $request = $data; + + // Fetch the label profile + /** @var LabelProfileRepository $profileRepo */ + $profileRepo = $this->entityManager->getRepository(LabelProfile::class); + $profile = $profileRepo->find($request->profileId); + if (!$profile instanceof LabelProfile) { + throw new NotFoundHttpException(sprintf('Label profile with ID %d not found.', $request->profileId)); + } + + // Check if user has read permission for the profile + if (!$this->security->isGranted('read', $profile)) { + throw new AccessDeniedHttpException('You do not have permission to access this label profile.'); + } + + // Get label options from profile + $options = $profile->getOptions(); + + // Override element type if provided, otherwise use profile's default + if ($request->elementType !== null) { + $options->setSupportedElement($request->elementType); + } + + // Parse element IDs from the range string + try { + $idArray = $this->rangeParser->parse($request->elementIds); + } catch (\InvalidArgumentException $e) { + throw new BadRequestHttpException('Invalid element IDs format: ' . $e->getMessage()); + } + + if (empty($idArray)) { + throw new BadRequestHttpException('No valid element IDs provided.'); + } + + // Fetch the target entities + /** @var DBElementRepository $repo */ + $repo = $this->entityManager->getRepository($options->getSupportedElement()->getEntityClass()); + + $elements = $repo->getElementsFromIDArray($idArray); + + if (empty($elements)) { + throw new NotFoundHttpException('No elements found with the provided IDs.'); + } + + // Generate the PDF + try { + $pdfContent = $this->labelGenerator->generateLabel($options, $elements); + } catch (\Exception $e) { + throw new BadRequestHttpException('Failed to generate label: ' . $e->getMessage()); + } + + // Generate filename + $filename = $this->generateFilename($elements[0], $profile); + + + // Return PDF as response + return new Response( + $pdfContent, + Response::HTTP_OK, + [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => sprintf('attachment; filename="%s"', $filename), + 'Content-Length' => (string) strlen($pdfContent), + ] + ); + } + + private function generateFilename(AbstractDBElement $element, LabelProfile $profile): string + { + $ret = 'label_' . $this->elementTypeNameGenerator->typeLabel($element); + $ret .= $element->getID(); + $ret .= '_' . preg_replace('/[^a-z0-9_\-]/i', '_', $profile->getName()); + + return $ret . '.pdf'; + } +} diff --git a/src/Twig/Sandbox/SandboxedLabelExtension.php b/src/Twig/Sandbox/SandboxedLabelExtension.php index 3b8eeed2..6fe85e80 100644 --- a/src/Twig/Sandbox/SandboxedLabelExtension.php +++ b/src/Twig/Sandbox/SandboxedLabelExtension.php @@ -65,28 +65,28 @@ class SandboxedLabelExtension extends AbstractExtension */ public function associatedParts(AbstractPartsContainingDBElement $element): array { - /** @var AbstractPartsContainingRepository $repo */ + /** @var AbstractPartsContainingRepository $repo */ $repo = $this->em->getRepository($element::class); return $repo->getParts($element); } public function associatedPartsCount(AbstractPartsContainingDBElement $element): int { - /** @var AbstractPartsContainingRepository $repo */ + /** @var AbstractPartsContainingRepository $repo */ $repo = $this->em->getRepository($element::class); return $repo->getPartsCount($element); } public function associatedPartsRecursive(AbstractPartsContainingDBElement $element): array { - /** @var AbstractPartsContainingRepository $repo */ + /** @var AbstractPartsContainingRepository $repo */ $repo = $this->em->getRepository($element::class); return $repo->getPartsRecursive($element); } public function associatedPartsCountRecursive(AbstractPartsContainingDBElement $element): int { - /** @var AbstractPartsContainingRepository $repo */ + /** @var AbstractPartsContainingRepository $repo */ $repo = $this->em->getRepository($element::class); return $repo->getPartsCountRecursive($element); } diff --git a/templates/form/extended_bootstrap_layout.html.twig b/templates/form/extended_bootstrap_layout.html.twig index ecd7caf0..1227750c 100644 --- a/templates/form/extended_bootstrap_layout.html.twig +++ b/templates/form/extended_bootstrap_layout.html.twig @@ -155,3 +155,8 @@ {{- parent() -}} {% endif %} {% endblock %} + +{% block boolean_constraint_widget %} + {{ form_widget(form.value) }} + {{ form_errors(form.value) }} +{% endblock %} diff --git a/templates/parts/edit/_eda.html.twig b/templates/parts/edit/_eda.html.twig index 4df675c4..1383871e 100644 --- a/templates/parts/edit/_eda.html.twig +++ b/templates/parts/edit/_eda.html.twig @@ -1,11 +1,7 @@ {{ form_row(form.eda_info.reference_prefix) }} {{ form_row(form.eda_info.value) }} -
-
- {{ form_row(form.eda_info.visibility) }} -
-
+{{ form_row(form.eda_info.visibility) }}
@@ -21,4 +17,4 @@
{{ form_row(form.eda_info.kicad_symbol) }} -{{ form_row(form.eda_info.kicad_footprint) }} \ No newline at end of file +{{ form_row(form.eda_info.kicad_footprint) }} diff --git a/tests/API/Endpoints/LabelEndpointTest.php b/tests/API/Endpoints/LabelEndpointTest.php new file mode 100644 index 00000000..338af836 --- /dev/null +++ b/tests/API/Endpoints/LabelEndpointTest.php @@ -0,0 +1,186 @@ +. + */ + +declare(strict_types=1); + +namespace App\Tests\API\Endpoints; + +use App\Tests\API\AuthenticatedApiTestCase; + +class LabelEndpointTest extends AuthenticatedApiTestCase +{ + public function testGetLabelProfiles(): void + { + $response = self::createAuthenticatedClient()->request('GET', '/api/label_profiles'); + + self::assertResponseIsSuccessful(); + self::assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); + + // Check that we get an array of label profiles + $json = $response->toArray(); + self::assertIsArray($json['hydra:member']); + self::assertNotEmpty($json['hydra:member']); + + // Check the structure of the first profile + $firstProfile = $json['hydra:member'][0]; + self::assertArrayHasKey('@id', $firstProfile); + self::assertArrayHasKey('name', $firstProfile); + self::assertArrayHasKey('options', $firstProfile); + self::assertArrayHasKey('show_in_dropdown', $firstProfile); + } + + public function testGetSingleLabelProfile(): void + { + $response = self::createAuthenticatedClient()->request('GET', '/api/label_profiles/1'); + + self::assertResponseIsSuccessful(); + self::assertJsonContains([ + '@id' => '/api/label_profiles/1', + ]); + + $json = $response->toArray(); + self::assertArrayHasKey('name', $json); + self::assertArrayHasKey('options', $json); + // Note: options is serialized but individual fields like width/height + // are only available in 'extended' or 'full' serialization groups + self::assertIsArray($json['options']); + } + + public function testFilterLabelProfilesByElementType(): void + { + $response = self::createAuthenticatedClient()->request('GET', '/api/label_profiles?options.supported_element=part'); + + self::assertResponseIsSuccessful(); + + $json = $response->toArray(); + // Check that we get results - the filter should work even if the field isn't in response + self::assertIsArray($json['hydra:member']); + // verify we got profiles + self::assertNotEmpty($json['hydra:member']); + } + + public function testGenerateLabelPdf(): void + { + $response = self::createAuthenticatedClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => '1', + ], + ]); + + self::assertResponseIsSuccessful(); + self::assertResponseHeaderSame('content-type', 'application/pdf'); + + // Check that the response contains PDF data + $content = $response->getContent(); + self::assertStringStartsWith('%PDF-', $content); + + // Check Content-Disposition header contains attachment and .pdf + $headers = $response->getHeaders(); + self::assertArrayHasKey('content-disposition', $headers); + $disposition = $headers['content-disposition'][0]; + self::assertStringContainsString('attachment', $disposition); + self::assertStringContainsString('.pdf', $disposition); + } + + public function testGenerateLabelPdfWithMultipleElements(): void + { + $response = self::createAuthenticatedClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => '1,2,3', + ], + ]); + + self::assertResponseIsSuccessful(); + self::assertResponseHeaderSame('content-type', 'application/pdf'); + self::assertStringStartsWith('%PDF-', $response->getContent()); + } + + public function testGenerateLabelPdfWithRange(): void + { + $response = self::createAuthenticatedClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => '1-3', + ], + ]); + + self::assertResponseIsSuccessful(); + self::assertResponseHeaderSame('content-type', 'application/pdf'); + self::assertStringStartsWith('%PDF-', $response->getContent()); + } + + public function testGenerateLabelPdfWithInvalidProfileId(): void + { + self::createAuthenticatedClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 99999, + 'elementIds' => '1', + ], + ]); + + self::assertResponseStatusCodeSame(404); + } + + public function testGenerateLabelPdfWithInvalidElementIds(): void + { + $client = self::createAuthenticatedClient(); + $client->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => 'invalid', + ], + ]); + + // Should return 400 or 422 (validation error) + $response = $client->getResponse(); + $statusCode = $response->getStatusCode(); + self::assertTrue( + $statusCode === 400 || $statusCode === 422, + "Expected status code 400 or 422, got {$statusCode}" + ); + } + + public function testGenerateLabelPdfWithNonExistentElements(): void + { + self::createAuthenticatedClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => '99999', + ], + ]); + + self::assertResponseStatusCodeSame(404); + } + + public function testGenerateLabelPdfRequiresAuthentication(): void + { + // Create a non-authenticated client + self::createClient()->request('POST', '/api/labels/generate', [ + 'json' => [ + 'profileId' => 1, + 'elementIds' => '1', + ], + ]); + + self::assertResponseStatusCodeSame(401); + } +} diff --git a/translations/frontend.da.xlf b/translations/frontend.da.xlf index 9c6b3129..4b6a15b9 100644 --- a/translations/frontend.da.xlf +++ b/translations/frontend.da.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/frontend.de.xlf b/translations/frontend.de.xlf index 4eaded60..9ebd0d32 100644 --- a/translations/frontend.de.xlf +++ b/translations/frontend.de.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/frontend.en.xlf b/translations/frontend.en.xlf index aa3cf2d9..91617f79 100644 --- a/translations/frontend.en.xlf +++ b/translations/frontend.en.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/frontend.hu.xlf b/translations/frontend.hu.xlf index bdcda170..c303dedc 100644 --- a/translations/frontend.hu.xlf +++ b/translations/frontend.hu.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/frontend.uk.xlf b/translations/frontend.uk.xlf index 86f51f95..fee1b03e 100644 --- a/translations/frontend.uk.xlf +++ b/translations/frontend.uk.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/frontend.zh.xlf b/translations/frontend.zh.xlf index d2ea6fd0..8bb063b8 100644 --- a/translations/frontend.zh.xlf +++ b/translations/frontend.zh.xlf @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index c4f2d5d8..c20e8152 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -11868,7 +11868,7 @@ Buerklin-API-Authentication-Server: update_manager.view_release - update_manager.view_release + Release ansehen @@ -11964,7 +11964,7 @@ Buerklin-API-Authentication-Server: update_manager.view_release_notes - update_manager.view_release_notes + Release notes ansehen @@ -12102,7 +12102,7 @@ Buerklin-API-Authentication-Server: perm.system.manage_updates - perm.system.manage_updates + Part-DB Updated verwalten @@ -12354,13 +12354,13 @@ Buerklin-API-Authentication-Server: settings.ips.generic_web_provider.enabled.help - settings.ips.generic_web_provider.enabled.help + Wenn der Anbieter aktiviert ist, können Benutzer im Namen des Part-DB-Servers Anfragen an beliebige Websites stellen. Aktivieren Sie diese Option nur, wenn Sie sich der möglichen Folgen bewusst sind. info_providers.from_url.title - Erstelle [part] aus URL + Erstelle [Part] aus URL @@ -12399,5 +12399,113 @@ Buerklin-API-Authentication-Server: Update zu + + + part.gtin + GTIN / EAN + + + + + info_providers.capabilities.gtin + GTIN / EAN + + + + + part.table.gtin + GTIN + + + + + scan_dialog.mode.gtin + GTIN / EAN Barcode + + + + + attachment_type.edit.allowed_targets + Nur verwenden für + + + + + attachment_type.edit.allowed_targets.help + Machen Sie diesen Anhangstyp nur für bestimmte Elementtypen verfügbar. Leer lassen, um diesen Anhangstyp für alle Elementtypen anzuzeigen. + + + + + orderdetails.edit.prices_includes_vat + Preise einschl. MwSt. + + + + + prices.incl_vat + Inkl. MwSt. + + + + + prices.excl_vat + Exkl. MwSt. + + + + + settings.system.localization.prices_include_tax_by_default + Preise enthalten standardmäßig Mehrwertsteuer + + + + + settings.system.localization.prices_include_tax_by_default.description + Der Standardwert für neu erstellte Einkaufinformationen, ob die Preise Mehrwertsteuer enthalten oder nicht. + + + + + part_lot.edit.last_stocktake_at + Letzte Inventur + + + + + perm.parts_stock.stocktake + Inventur + + + + + part.info.stocktake_modal.title + Inventur des Bestandes + + + + + part.info.stocktake_modal.expected_amount + Erwartete Menge + + + + + part.info.stocktake_modal.actual_amount + Tatsächliche Menge + + + + + log.part_stock_changed.stock_take + Inventur + + + + + log.element_edited.changed_fields.last_stocktake_at + Letzte Inventur + + - \ No newline at end of file + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index bbd96ac6..d9418563 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12402,109 +12402,109 @@ Buerklin-API Authentication server: - + part.gtin GTIN / EAN - + info_providers.capabilities.gtin GTIN / EAN - + part.table.gtin GTIN - + scan_dialog.mode.gtin GTIN / EAN barcode - + attachment_type.edit.allowed_targets Use only for - + attachment_type.edit.allowed_targets.help Make this attachment type only available for certain element classes. Leave empty to show this attachment type for all element classes. - + orderdetails.edit.prices_includes_vat Prices include VAT - + prices.incl_vat Incl. VAT - + prices.excl_vat Excl. VAT - + settings.system.localization.prices_include_tax_by_default Prices include VAT by default - + settings.system.localization.prices_include_tax_by_default.description The default value for newly created purchase infos, if prices include VAT or not. - + part_lot.edit.last_stocktake_at Last stocktake - + perm.parts_stock.stocktake Stocktake - + part.info.stocktake_modal.title Stocktake lot - + part.info.stocktake_modal.expected_amount Expected amount - + part.info.stocktake_modal.actual_amount Actual amount - + log.part_stock_changed.stock_take Stocktake - + log.element_edited.changed_fields.last_stocktake_at Last stocktake diff --git a/translations/security.da.xlf b/translations/security.da.xlf index 99329533..ab35c605 100644 --- a/translations/security.da.xlf +++ b/translations/security.da.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.de.xlf b/translations/security.de.xlf index 2a357094..927f8f9c 100644 --- a/translations/security.de.xlf +++ b/translations/security.de.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.en.xlf b/translations/security.en.xlf index 5a79d6ec..0b0b4569 100644 --- a/translations/security.en.xlf +++ b/translations/security.en.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.hu.xlf b/translations/security.hu.xlf index 3c885815..7c448da0 100644 --- a/translations/security.hu.xlf +++ b/translations/security.hu.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.nl.xlf b/translations/security.nl.xlf index 0e4ecc41..7ba9fcc1 100644 --- a/translations/security.nl.xlf +++ b/translations/security.nl.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.uk.xlf b/translations/security.uk.xlf index 03be9410..12737cf3 100644 --- a/translations/security.uk.xlf +++ b/translations/security.uk.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/security.zh.xlf b/translations/security.zh.xlf index 181c9c0f..58fbb26f 100644 --- a/translations/security.zh.xlf +++ b/translations/security.zh.xlf @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index ed824f0b..624c6a89 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -248,7 +248,7 @@ - + validator.invalid_gtin This is not an valid GTIN / EAN! diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 03942667..e5392d76 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -148,7 +148,7 @@ project.bom_has_to_include_all_subelement_parts - BOM projektu musi zawierać wszystkie komponenty produkcyjne podprojektów. Brakuje komponentu %part_name% projektu %project_name%! + BOM projektu musi zawierać wszystkie komponenty produkcyjne pod projektów @@ -223,6 +223,12 @@ Ze względu na ograniczenia techniczne nie jest możliwe wybranie daty po 19 stycznia 2038 w systemach 32-bitowych! + + + validator.fileSize.invalidFormat + Niewłaściwy format + + validator.invalid_range @@ -235,5 +241,11 @@ Nieprawidłowy kod. Sprawdź, czy aplikacja uwierzytelniająca jest poprawnie skonfigurowana i czy zarówno serwer, jak i urządzenie uwierzytelniające mają poprawnie ustawiony czas. + + + settings.synonyms.type_synonyms.collection_type.duplicate + Duplikuj + + - \ No newline at end of file + diff --git a/yarn.lock b/yarn.lock index e890f0fe..e3d72ad7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2800,9 +2800,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001759: - version "1.0.30001769" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz#1ad91594fad7dc233777c2781879ab5409f7d9c2" - integrity sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg== + version "1.0.30001770" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz#4dc47d3b263a50fbb243448034921e0a88591a84" + integrity sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw== ccount@^2.0.0: version "2.0.1" @@ -7855,9 +7855,9 @@ webpack-sources@^2.0.1, webpack-sources@^2.2.0: source-map "^0.6.1" webpack-sources@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723" - integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg== + version "3.3.4" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.4.tgz#a338b95eb484ecc75fbb196cbe8a2890618b4891" + integrity sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q== webpack@^5.74.0: version "5.105.2"