diff --git a/src/Controller/ScanController.php b/src/Controller/ScanController.php index 77183c89..aebadd89 100644 --- a/src/Controller/ScanController.php +++ b/src/Controller/ScanController.php @@ -42,10 +42,10 @@ declare(strict_types=1); namespace App\Controller; use App\Form\LabelSystem\ScanDialogType; -use App\Services\LabelSystem\Barcodes\BarcodeScanHelper; -use App\Services\LabelSystem\Barcodes\BarcodeRedirector; -use App\Services\LabelSystem\Barcodes\BarcodeScanResult; -use App\Services\LabelSystem\Barcodes\BarcodeSourceType; +use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector; +use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper; +use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType; +use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult; use Doctrine\ORM\EntityNotFoundException; use InvalidArgumentException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -77,13 +77,21 @@ class ScanController extends AbstractController $mode = $form['mode']->getData(); } + $infoModeData = null; + if ($input !== null) { try { $scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null); - try { - return $this->redirect($this->barcodeParser->getRedirectURL($scan_result)); - } catch (EntityNotFoundException) { - $this->addFlash('success', 'scan.qr_not_found'); + //Perform a redirect if the info mode is not enabled + if (!$form['info_mode']->getData()) { + try { + return $this->redirect($this->barcodeParser->getRedirectURL($scan_result)); + } catch (EntityNotFoundException) { + $this->addFlash('success', 'scan.qr_not_found'); + } + } else { //Otherwise retrieve infoModeData + $infoModeData = $scan_result->getDecodedForInfoMode(); + } } catch (InvalidArgumentException) { $this->addFlash('error', 'scan.format_unknown'); @@ -92,6 +100,7 @@ class ScanController extends AbstractController return $this->render('label_system/scanner/scanner.html.twig', [ 'form' => $form, + 'infoModeData' => $infoModeData, ]); } @@ -109,7 +118,7 @@ class ScanController extends AbstractController throw new InvalidArgumentException('Unknown type: '.$type); } //Construct the scan result manually, as we don't have a barcode here - $scan_result = new BarcodeScanResult( + $scan_result = new LocalBarcodeScanResult( target_type: BarcodeScanHelper::QR_TYPE_MAP[$type], target_id: $id, //The routes are only used on the internal generated QR codes diff --git a/src/DataFixtures/PartFixtures.php b/src/DataFixtures/PartFixtures.php index a9290cbf..0c8ea36d 100644 --- a/src/DataFixtures/PartFixtures.php +++ b/src/DataFixtures/PartFixtures.php @@ -106,7 +106,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface $partLot2->setComment('Test'); $partLot2->setNeedsRefill(true); $partLot2->setStorageLocation($manager->find(StorageLocation::class, 3)); - $partLot2->setVendorBarcode('lot2_vendor_barcode'); + $partLot2->setUserBarcode('lot2_vendor_barcode'); $part->addPartLot($partLot2); $orderdetail = new Orderdetail(); diff --git a/src/Entity/Parts/PartLot.php b/src/Entity/Parts/PartLot.php index c1b7da8a..d893e6de 100644 --- a/src/Entity/Parts/PartLot.php +++ b/src/Entity/Parts/PartLot.php @@ -68,7 +68,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')] #[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')] #[ValidPartLot] -#[UniqueEntity(['vendor_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')] +#[UniqueEntity(['user_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')] #[ApiResource( operations: [ new Get(security: 'is_granted("read", object)'), @@ -166,10 +166,10 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named /** * @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor) */ - #[ORM\Column(type: Types::STRING, nullable: true)] + #[ORM\Column(name: "vendor_barcode", type: Types::STRING, nullable: true)] #[Groups(['part_lot:read', 'part_lot:write'])] #[Length(max: 255)] - protected ?string $vendor_barcode = null; + protected ?string $user_barcode = null; public function __clone() { @@ -375,19 +375,19 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named * null if no barcode is set. * @return string|null */ - public function getVendorBarcode(): ?string + public function getUserBarcode(): ?string { - return $this->vendor_barcode; + return $this->user_barcode; } /** * Set the content of the barcode of this part lot (e.g. a barcode on the package put by the vendor). - * @param string|null $vendor_barcode + * @param string|null $user_barcode * @return $this */ - public function setVendorBarcode(?string $vendor_barcode): PartLot + public function setUserBarcode(?string $user_barcode): PartLot { - $this->vendor_barcode = $vendor_barcode; + $this->user_barcode = $user_barcode; return $this; } diff --git a/src/Form/LabelSystem/ScanDialogType.php b/src/Form/LabelSystem/ScanDialogType.php index 5230dd5a..13ff8e6f 100644 --- a/src/Form/LabelSystem/ScanDialogType.php +++ b/src/Form/LabelSystem/ScanDialogType.php @@ -41,8 +41,9 @@ declare(strict_types=1); namespace App\Form\LabelSystem; -use App\Services\LabelSystem\Barcodes\BarcodeSourceType; +use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -55,6 +56,8 @@ class ScanDialogType extends AbstractType { $builder->add('input', TextType::class, [ 'label' => 'scan_dialog.input', + //Do not trim the input, otherwise this damages Format06 barcodes which end with non-printable characters + 'trim' => false, 'attr' => [ 'autofocus' => true, 'id' => 'scan_dialog_input', @@ -71,9 +74,14 @@ class ScanDialogType extends AbstractType null => 'scan_dialog.mode.auto', BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal', BarcodeSourceType::IPN => 'scan_dialog.mode.ipn', - BarcodeSourceType::VENDOR => 'scan_dialog.mode.vendor', + BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user', + BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp' }, + ]); + $builder->add('info_mode', CheckboxType::class, [ + 'label' => 'scan_dialog.info_mode', + 'required' => false, ]); $builder->add('submit', SubmitType::class, [ diff --git a/src/Form/Part/PartLotType.php b/src/Form/Part/PartLotType.php index da061c7f..785810a6 100644 --- a/src/Form/Part/PartLotType.php +++ b/src/Form/Part/PartLotType.php @@ -103,8 +103,8 @@ class PartLotType extends AbstractType 'help' => 'part_lot.owner.help', ]); - $builder->add('vendor_barcode', TextType::class, [ - 'label' => 'part_lot.edit.vendor_barcode', + $builder->add('user_barcode', TextType::class, [ + 'label' => 'part_lot.edit.user_barcode', 'help' => 'part_lot.edit.vendor_barcode.help', 'required' => false, ]); diff --git a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php index ac971ba9..3c1e4cca 100644 --- a/src/Services/InfoProviderSystem/DTOtoEntityConverter.php +++ b/src/Services/InfoProviderSystem/DTOtoEntityConverter.php @@ -160,9 +160,8 @@ final class DTOtoEntityConverter //Try to map the category to an existing entity (but never create a new one) if ($dto->category) { - /** @var CategoryRepository $categoryRepo */ - $categoryRepo = $this->em->getRepository(Category::class); - $entity->setCategory($categoryRepo->findForInfoProvider($dto->category)); + //@phpstan-ignore-next-line For some reason php does not recognize the repo returns a category + $entity->setCategory($this->em->getRepository(Category::class)->findForInfoProvider($dto->category)); } $entity->setManufacturer($this->getOrCreateEntity(Manufacturer::class, $dto->manufacturer)); diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php new file mode 100644 index 00000000..2de7c035 --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php @@ -0,0 +1,166 @@ +. + */ + +declare(strict_types=1); + +/** + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 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 . + */ + +namespace App\Services\LabelSystem\BarcodeScanner; + +use App\Entity\LabelSystem\LabelSupportedElement; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartLot; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityNotFoundException; +use InvalidArgumentException; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +/** + * @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeRedirectorTest + */ +final class BarcodeRedirector +{ + public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em) + { + } + + /** + * Determines the URL to which the user should be redirected, when scanning a QR code. + * + * @param BarcodeScanResultInterface $barcodeScan The result of the barcode scan + * @return string the URL to which should be redirected + * + * @throws EntityNotFoundException + */ + public function getRedirectURL(BarcodeScanResultInterface $barcodeScan): string + { + if($barcodeScan instanceof LocalBarcodeScanResult) { + return $this->getURLLocalBarcode($barcodeScan); + } + + if ($barcodeScan instanceof EIGP114BarcodeScanResult) { + return $this->getURLVendorBarcode($barcodeScan); + } + + throw new InvalidArgumentException('Unknown $barcodeScan type: '.get_class($barcodeScan)); + } + + private function getURLLocalBarcode(LocalBarcodeScanResult $barcodeScan): string + { + switch ($barcodeScan->target_type) { + case LabelSupportedElement::PART: + return $this->urlGenerator->generate('app_part_show', ['id' => $barcodeScan->target_id]); + case LabelSupportedElement::PART_LOT: + //Try to determine the part to the given lot + $lot = $this->em->find(PartLot::class, $barcodeScan->target_id); + if (!$lot instanceof PartLot) { + throw new EntityNotFoundException(); + } + + return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID()]); + + case LabelSupportedElement::STORELOCATION: + return $this->urlGenerator->generate('part_list_store_location', ['id' => $barcodeScan->target_id]); + + default: + throw new InvalidArgumentException('Unknown $type: '.$barcodeScan->target_type->name); + } + } + + /** + * Gets the URL to a part from a scan of a Vendor Barcode + */ + private function getURLVendorBarcode(EIGP114BarcodeScanResult $barcodeScan): string + { + $part = $this->getPartFromVendor($barcodeScan); + return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]); + } + + /** + * Gets a part from a scan of a Vendor Barcode by filtering for parts + * with the same Info Provider Id or, if that fails, by looking for parts with a + * matching manufacturer product number. Only returns the first matching part. + */ + private function getPartFromVendor(EIGP114BarcodeScanResult $barcodeScan) : Part + { + // first check via the info provider ID (e.g. Vendor ID). This might fail if the part was not added via + // the info provider system or if the part was bought from a different vendor than the data was retrieved + // from. + if($barcodeScan->digikeyPartNumber) { + $qb = $this->em->getRepository(Part::class)->createQueryBuilder('part'); + //Lower() to be case insensitive + $qb->where($qb->expr()->like('LOWER(part.providerReference.provider_id)', 'LOWER(:vendor_id)')); + $qb->setParameter('vendor_id', $barcodeScan->digikeyPartNumber); + $results = $qb->getQuery()->getResult(); + if ($results) { + return $results[0]; + } + } + + if(!$barcodeScan->supplierPartNumber){ + throw new EntityNotFoundException(); + } + + //Fallback to the manufacturer part number. This may return false positives, since it is common for + //multiple manufacturers to use the same part number for their version of a common product + //We assume the user is able to realize when this returns the wrong part + //If the barcode specifies the manufacturer we try to use that as well + $mpnQb = $this->em->getRepository(Part::class)->createQueryBuilder('part'); + $mpnQb->where($mpnQb->expr()->like('LOWER(part.manufacturer_product_number)', 'LOWER(:mpn)')); + $mpnQb->setParameter('mpn', $barcodeScan->supplierPartNumber); + + if($barcodeScan->mouserManufacturer){ + $manufacturerQb = $this->em->getRepository(Manufacturer::class)->createQueryBuilder("manufacturer"); + $manufacturerQb->where($manufacturerQb->expr()->like("LOWER(manufacturer.name)", "LOWER(:manufacturer_name)")); + $manufacturerQb->setParameter("manufacturer_name", $barcodeScan->mouserManufacturer); + $manufacturers = $manufacturerQb->getQuery()->getResult(); + + if($manufacturers) { + $mpnQb->andWhere($mpnQb->expr()->eq("part.manufacturer", ":manufacturer")); + $mpnQb->setParameter("manufacturer", $manufacturers); + } + + } + + $results = $mpnQb->getQuery()->getResult(); + if($results){ + return $results[0]; + } + throw new EntityNotFoundException(); + } +} diff --git a/src/Services/LabelSystem/Barcodes/BarcodeScanHelper.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php similarity index 81% rename from src/Services/LabelSystem/Barcodes/BarcodeScanHelper.php rename to src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php index c9750ea3..6d2cb8c9 100644 --- a/src/Services/LabelSystem/Barcodes/BarcodeScanHelper.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php @@ -39,7 +39,7 @@ declare(strict_types=1); * along with this program. If not, see . */ -namespace App\Services\LabelSystem\Barcodes; +namespace App\Services\LabelSystem\BarcodeScanner; use App\Entity\LabelSystem\LabelSupportedElement; use App\Entity\Parts\Part; @@ -75,30 +75,39 @@ final class BarcodeScanHelper * will try to guess the type. * @param string $input * @param BarcodeSourceType|null $type - * @return BarcodeScanResult + * @return BarcodeScanResultInterface */ - public function scanBarcodeContent(string $input, ?BarcodeSourceType $type = null): BarcodeScanResult + public function scanBarcodeContent(string $input, ?BarcodeSourceType $type = null): BarcodeScanResultInterface { //Do specific parsing if ($type === BarcodeSourceType::INTERNAL) { return $this->parseInternalBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); } - if ($type === BarcodeSourceType::VENDOR) { - return $this->parseVendorBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); + if ($type === BarcodeSourceType::USER_DEFINED) { + return $this->parseUserDefinedBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); } if ($type === BarcodeSourceType::IPN) { return $this->parseIPNBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); } + if ($type === BarcodeSourceType::EIGP114) { + return $this->parseEIGP114Barcode($input); + } //Null means auto and we try the different formats + + //If the barcode is formatted as EIGP114, we can parse it directly + if (EIGP114BarcodeScanResult::isFormat06Code($input)) { + return $this->parseEIGP114Barcode($input); + } + $result = $this->parseInternalBarcode($input); if ($result !== null) { return $result; } - //Try to parse as vendor barcode - $result = $this->parseVendorBarcode($input); + //Try to parse as User defined barcode + $result = $this->parseUserDefinedBarcode($input); if ($result !== null) { return $result; } @@ -112,11 +121,16 @@ final class BarcodeScanHelper throw new InvalidArgumentException('Unknown barcode'); } - private function parseVendorBarcode(string $input): ?BarcodeScanResult + private function parseEIGP114Barcode(string $input): EIGP114BarcodeScanResult + { + return EIGP114BarcodeScanResult::parseFormat06Code($input); + } + + private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult { $lot_repo = $this->entityManager->getRepository(PartLot::class); //Find only the first result - $results = $lot_repo->findBy(['vendor_barcode' => $input], limit: 1); + $results = $lot_repo->findBy(['user_barcode' => $input], limit: 1); if (count($results) === 0) { return null; @@ -124,14 +138,14 @@ final class BarcodeScanHelper //We found a part, so use it to create the result $lot = $results[0]; - return new BarcodeScanResult( + return new LocalBarcodeScanResult( target_type: LabelSupportedElement::PART_LOT, target_id: $lot->getID(), - source_type: BarcodeSourceType::VENDOR + source_type: BarcodeSourceType::USER_DEFINED ); } - private function parseIPNBarcode(string $input): ?BarcodeScanResult + private function parseIPNBarcode(string $input): ?LocalBarcodeScanResult { $part_repo = $this->entityManager->getRepository(Part::class); //Find only the first result @@ -143,7 +157,7 @@ final class BarcodeScanHelper //We found a part, so use it to create the result $part = $results[0]; - return new BarcodeScanResult( + return new LocalBarcodeScanResult( target_type: LabelSupportedElement::PART, target_id: $part->getID(), source_type: BarcodeSourceType::IPN @@ -155,9 +169,9 @@ final class BarcodeScanHelper * If the barcode could not be parsed at all, null is returned. If the barcode is a valid format, but could * not be found in the database, an exception is thrown. * @param string $input - * @return BarcodeScanResult|null + * @return LocalBarcodeScanResult|null */ - private function parseInternalBarcode(string $input): ?BarcodeScanResult + private function parseInternalBarcode(string $input): ?LocalBarcodeScanResult { $input = trim($input); $matches = []; @@ -167,7 +181,7 @@ final class BarcodeScanHelper //Extract parts from QR code's URL if (preg_match('#^https?://.*/scan/(\w+)/(\d+)/?$#', $input, $matches)) { - return new BarcodeScanResult( + return new LocalBarcodeScanResult( target_type: self::QR_TYPE_MAP[strtolower($matches[1])], target_id: (int) $matches[2], source_type: BarcodeSourceType::INTERNAL @@ -183,7 +197,7 @@ final class BarcodeScanHelper throw new InvalidArgumentException('Unknown prefix '.$prefix); } - return new BarcodeScanResult( + return new LocalBarcodeScanResult( target_type: self::PREFIX_TYPE_MAP[$prefix], target_id: $id, source_type: BarcodeSourceType::INTERNAL @@ -199,7 +213,7 @@ final class BarcodeScanHelper throw new InvalidArgumentException('Unknown prefix '.$prefix); } - return new BarcodeScanResult( + return new LocalBarcodeScanResult( target_type: self::PREFIX_TYPE_MAP[$prefix], target_id: $id, source_type: BarcodeSourceType::INTERNAL @@ -208,7 +222,7 @@ final class BarcodeScanHelper //Legacy Part-DB location labels used $L00336 format if (preg_match('#^\$L(\d{5,})$#', $input, $matches)) { - return new BarcodeScanResult( + return new LocalBarcodeScanResult( target_type: LabelSupportedElement::STORELOCATION, target_id: (int) $matches[1], source_type: BarcodeSourceType::INTERNAL @@ -217,7 +231,7 @@ final class BarcodeScanHelper //Legacy Part-DB used EAN8 barcodes for part labels. Format 0000001(2) (note the optional 8th digit => checksum) if (preg_match('#^(\d{7})\d?$#', $input, $matches)) { - return new BarcodeScanResult( + return new LocalBarcodeScanResult( target_type: LabelSupportedElement::PART, target_id: (int) $matches[1], source_type: BarcodeSourceType::INTERNAL diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultInterface.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultInterface.php new file mode 100644 index 00000000..88130351 --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultInterface.php @@ -0,0 +1,36 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\BarcodeScanner; + +interface BarcodeScanResultInterface +{ + /** + * Returns all data that was decoded from the barcode in a format, that can be shown in a table to the user. + * The return values of this function are not meant to be parsed by code again, but should just give a information + * to the user. + * The keys of the returned array are the first column of the table and the values are the second column. + * @return array + */ + public function getDecodedForInfoMode(): array; +} \ No newline at end of file diff --git a/src/Services/LabelSystem/Barcodes/BarcodeSourceType.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php similarity index 83% rename from src/Services/LabelSystem/Barcodes/BarcodeSourceType.php rename to src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php index 20ba316a..40f707de 100644 --- a/src/Services/LabelSystem/Barcodes/BarcodeSourceType.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php @@ -21,7 +21,7 @@ declare(strict_types=1); -namespace App\Services\LabelSystem\Barcodes; +namespace App\Services\LabelSystem\BarcodeScanner; /** * This enum represents the different types, where a barcode/QR-code can be generated from @@ -32,9 +32,14 @@ enum BarcodeSourceType case INTERNAL; /** This barcode is containing an internal part number (IPN) */ case IPN; + /** - * This barcode is a custom barcode from a third party like a vendor, which was set via the vendor_barcode - * field of a part lot. + * This barcode is a user defined barcode defined on a part lot */ - case VENDOR; + case USER_DEFINED; + + /** + * EIGP114 formatted barcodes like used by digikey, mouser, etc. + */ + case EIGP114; } \ No newline at end of file diff --git a/src/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResult.php new file mode 100644 index 00000000..0b4f4b56 --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResult.php @@ -0,0 +1,332 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\LabelSystem\BarcodeScanner; + +/** + * This class represents the content of a EIGP114 barcode. + * Based on PR 811, EIGP 114.2018 (https://www.ecianow.org/assets/docs/GIPC/EIGP-114.2018%20ECIA%20Labeling%20Specification%20for%20Product%20and%20Shipment%20Identification%20in%20the%20Electronics%20Industry%20-%202D%20Barcode.pdf), + * , https://forum.digikey.com/t/digikey-product-labels-decoding-digikey-barcodes/41097 + */ +class EIGP114BarcodeScanResult implements BarcodeScanResultInterface +{ + + /** + * @var string|null Ship date in format YYYYMMDD + */ + public readonly ?string $shipDate; + + /** + * @var string|null Customer assigned part number – Optional based on + * agreements between Distributor and Supplier + */ + public readonly ?string $customerPartNumber; + + /** + * @var string|null Supplier assigned part number + */ + public readonly ?string $supplierPartNumber; + + /** + * @var int|null Quantity of product + */ + public readonly ?int $quantity; + + /** + * @var string|null Customer assigned purchase order number + */ + public readonly ?string $customerPO; + + /** + * @var string|null Line item number from PO. Required on Logistic Label when + * used on back of Packing Slip. See Section 4.9 + */ + public readonly ?string $customerPOLine; + + /** + * 9D - YYWW (Year and Week of Manufacture). ) If no date code is used + * for a particular part, this field should be populated with N/T + * to indicate the product is Not Traceable by this data field. + * @var string|null + */ + public readonly ?string $dateCode; + + /** + * 10D - YYWW (Year and Week of Manufacture). ) If no date code is used + * for a particular part, this field should be populated with N/T + * to indicate the product is Not Traceable by this data field. + * @var string|null + */ + public readonly ?string $alternativeDateCode; + + /** + * Traceability number assigned to a batch or group of items. If + * no lot code is used for a particular part, this field should be + * populated with N/T to indicate the product is Not Traceable + * by this data field. + * @var string|null + */ + public readonly ?string $lotCode; + + /** + * Country where part was manufactured. Two-letter code from + * ISO 3166 country code list + * @var string|null + */ + public readonly ?string $countryOfOrigin; + + /** + * @var string|null Unique alphanumeric number assigned by supplier + * 3S - Package ID for Inner Pack when part of a mixed Logistic + * Carton. Always used in conjunction with a mixed logistic label + * with a 5S data identifier for Package ID. + */ + public readonly ?string $packageId1; + + /** + * @var string|null + * 4S - Package ID for Logistic Carton with like items + */ + public readonly ?string $packageId2; + + /** + * @var string|null + * 5S - Package ID for Logistic Carton with mixed items + */ + public readonly ?string $packageId3; + + /** + * @var string|null Unique alphanumeric number assigned by supplier. + */ + public readonly ?string $packingListNumber; + + /** + * @var string|null Ship date in format YYYYMMDD + */ + public readonly ?string $serialNumber; + + /** + * @var string|null Code for sorting and classifying LEDs. Use when applicable + */ + public readonly ?string $binCode; + + /** + * @var int|null Sequential carton count in format “#/#” or “# of #” + */ + public readonly ?int $packageCount; + + /** + * @var string|null Alphanumeric string assigned by the supplier to distinguish + * from one closely-related design variation to another. Use as + * required or when applicable + */ + public readonly ?string $revisionNumber; + + /** + * @var string|null Digikey Extension: This is not represented in the ECIA spec, but the field being used is found in the ANSI MH10.8.2-2016 spec on which the ECIA spec is based. In the ANSI spec it is called First Level (Supplier Assigned) Part Number. + */ + public readonly ?string $digikeyPartNumber; + + /** + * @var string|null Digikey Extension: This can be shared across multiple invoices and time periods and is generated as an order enters our system from any vector (web, API, phone order, etc.) + */ + public readonly ?string $digikeySalesOrderNumber; + + /** + * @var string|null Digikey extension: This is typically assigned per shipment as items are being released to be picked in the warehouse. A SO can have many Invoice numbers + */ + public readonly ?string $digikeyInvoiceNumber; + + /** + * @var string|null Digikey extension: This is for internal DigiKey purposes and defines the label type. + */ + public readonly ?string $digikeyLabelType; + + /** + * @var string|null You will also see this as the last part of a URL for a product detail page. Ex https://www.digikey.com/en/products/detail/w%C3%BCrth-elektronik/860010672008/5726907 + */ + public readonly ?string $digikeyPartID; + + /** + * @var string|null Digikey Extension: For internal use of Digikey. Probably not needed + */ + public readonly ?string $digikeyNA; + + /** + * @var string|null Digikey Extension: This is a field of varying length used to keep the barcode approximately the same size between labels. It is safe to ignore. + */ + public readonly ?string $digikeyPadding; + + public readonly ?string $mouserPositionInOrder; + + public readonly ?string $mouserManufacturer; + + + + /** + * + * @param array $data The fields of the EIGP114 barcode, where the key is the field name and the value is the field content + */ + public function __construct(public readonly array $data) + { + //IDs per EIGP 114.2018 + $this->shipDate = $data['6D'] ?? null; + $this->customerPartNumber = $data['P'] ?? null; + $this->supplierPartNumber = $data['1P'] ?? null; + $this->quantity = isset($data['Q']) ? (int)$data['Q'] : null; + $this->customerPO = $data['K'] ?? null; + $this->customerPOLine = $data['4K'] ?? null; + $this->dateCode = $data['9D'] ?? null; + $this->alternativeDateCode = $data['10D'] ?? null; + $this->lotCode = $data['1T'] ?? null; + $this->countryOfOrigin = $data['4L'] ?? null; + $this->packageId1 = $data['3S'] ?? null; + $this->packageId2 = $data['4S'] ?? null; + $this->packageId3 = $data['5S'] ?? null; + $this->packingListNumber = $data['11K'] ?? null; + $this->serialNumber = $data['S'] ?? null; + $this->binCode = $data['33P'] ?? null; + $this->packageCount = isset($data['13Q']) ? (int)$data['13Q'] : null; + $this->revisionNumber = $data['2P'] ?? null; + //IDs used by Digikey + $this->digikeyPartNumber = $data['30P'] ?? null; + $this->digikeySalesOrderNumber = $data['1K'] ?? null; + $this->digikeyInvoiceNumber = $data['10K'] ?? null; + $this->digikeyLabelType = $data['11Z'] ?? null; + $this->digikeyPartID = $data['12Z'] ?? null; + $this->digikeyNA = $data['13Z'] ?? null; + $this->digikeyPadding = $data['20Z'] ?? null; + //IDs used by Mouser + $this->mouserPositionInOrder = $data['14K'] ?? null; + $this->mouserManufacturer = $data['1V'] ?? null; + } + + /** + * Tries to guess the vendor of the barcode based on the supplied data field. + * This is experimental and should not be relied upon. + * @return string|null The guessed vendor as smallcase string (e.g. "digikey", "mouser", etc.), or null if the vendor could not be guessed + */ + public function guessBarcodeVendor(): ?string + { + //If the barcode data contains the digikey extensions, we assume it is a digikey barcode + if (isset($this->data['13Z']) || isset($this->data['20Z']) || isset($this->data['12Z']) || isset($this->data['11Z'])) { + return 'digikey'; + } + + //If the barcode data contains the mouser extensions, we assume it is a mouser barcode + if (isset($this->data['14K']) || isset($this->data['1V'])) { + return 'mouser'; + } + + //According to this thread (https://github.com/inventree/InvenTree/issues/853), Newark/element14 codes contains a "3P" field + if (isset($this->data['3P'])) { + return 'element14'; + } + + return null; + } + + /** + * Checks if the given input is a valid format06 formatted data. + * This just perform a simple check for the header, the content might be malformed still. + * @param string $input + * @return bool + */ + public static function isFormat06Code(string $input): bool + { + //Code must begin with [)>06 + if(!str_starts_with($input, "[)>\u{1E}06\u{1D}")){ + return false; + } + + //Digikey does not put a trailer onto the barcode, so we just check for the header + + return true; + } + + /** + * Parses a format06 code a returns a new instance of this class + * @param string $input + * @return self + */ + public static function parseFormat06Code(string $input): self + { + //Ensure that the input is a valid format06 code + if (!self::isFormat06Code($input)) { + throw new \InvalidArgumentException("The given input is not a valid format06 code"); + } + + //Remove the trailer, if present + if (str_ends_with($input, "\u{1E}\u{04}")){ + $input = substr($input, 5, -2); + } + + //Split the input into the different fields (using the separator) + $parts = explode("\u{1D}", $input); + + //The first field is the format identifier, which we do not need + array_shift($parts); + + //Split the fields into key-value pairs + $results = []; + + foreach($parts as $part) { + //^ 0* ([1-9]? \d* [A-Z]) + //Start of the string Leading zeros are discarded Not a zero Any number of digits single uppercase Letter + // 00 1 4 K + + if(!preg_match('/^0*([1-9]?\d*[A-Z])/', $part, $matches)) { + throw new \LogicException("Could not parse field: $part"); + } + //Extract the key + $key = $matches[0]; + //Extract the field value + $fieldValue = substr($part, strlen($matches[0])); + + $results[$key] = $fieldValue; + } + + return new self($results); + } + + public function getDecodedForInfoMode(): array + { + $tmp = [ + 'Barcode type' => 'EIGP114', + 'Guessed vendor from barcode' => $this->guessBarcodeVendor() ?? 'Unknown', + ]; + + //Iterate over all fields of this object and add them to the array if they are not null + foreach((array) $this as $key => $value) { + //Skip data key + if ($key === 'data') { + continue; + } + if($value !== null) { + $tmp[$key] = $value; + } + } + + return $tmp; + } +} \ No newline at end of file diff --git a/src/Services/LabelSystem/Barcodes/BarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/LocalBarcodeScanResult.php similarity index 66% rename from src/Services/LabelSystem/Barcodes/BarcodeScanResult.php rename to src/Services/LabelSystem/BarcodeScanner/LocalBarcodeScanResult.php index 7f1315b3..050aff6f 100644 --- a/src/Services/LabelSystem/Barcodes/BarcodeScanResult.php +++ b/src/Services/LabelSystem/BarcodeScanner/LocalBarcodeScanResult.php @@ -21,14 +21,15 @@ declare(strict_types=1); -namespace App\Services\LabelSystem\Barcodes; +namespace App\Services\LabelSystem\BarcodeScanner; use App\Entity\LabelSystem\LabelSupportedElement; /** - * This class represents the result of a barcode scan, with the target type and the ID of the element + * This class represents the result of a barcode scan of a barcode that uniquely identifies a local entity, + * like an internally generated barcode or a barcode that was added manually to the system by a user */ -class BarcodeScanResult +class LocalBarcodeScanResult implements BarcodeScanResultInterface { public function __construct( public readonly LabelSupportedElement $target_type, @@ -36,4 +37,13 @@ class BarcodeScanResult public readonly BarcodeSourceType $source_type, ) { } + + public function getDecodedForInfoMode(): array + { + return [ + 'Barcode type' => $this->source_type->name, + 'Target type' => $this->target_type->name, + 'Target ID' => $this->target_id, + ]; + } } \ No newline at end of file diff --git a/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php b/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php deleted file mode 100644 index bc21b787..00000000 --- a/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php +++ /dev/null @@ -1,89 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). - * - * Copyright (C) 2019 - 2022 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 . - */ - -namespace App\Services\LabelSystem\Barcodes; - -use App\Entity\LabelSystem\LabelSupportedElement; -use App\Entity\Parts\PartLot; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\EntityNotFoundException; -use InvalidArgumentException; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; - -/** - * @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeRedirectorTest - */ -final class BarcodeRedirector -{ - public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em) - { - } - - /** - * Determines the URL to which the user should be redirected, when scanning a QR code. - * - * @param BarcodeScanResult $barcodeScan The result of the barcode scan - * @return string the URL to which should be redirected - * - * @throws EntityNotFoundException - */ - public function getRedirectURL(BarcodeScanResult $barcodeScan): string - { - switch ($barcodeScan->target_type) { - case LabelSupportedElement::PART: - return $this->urlGenerator->generate('app_part_show', ['id' => $barcodeScan->target_id]); - case LabelSupportedElement::PART_LOT: - //Try to determine the part to the given lot - $lot = $this->em->find(PartLot::class, $barcodeScan->target_id); - if (!$lot instanceof PartLot) { - throw new EntityNotFoundException(); - } - - return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID()]); - - case LabelSupportedElement::STORELOCATION: - return $this->urlGenerator->generate('part_list_store_location', ['id' => $barcodeScan->target_id]); - - default: - throw new InvalidArgumentException('Unknown $type: '.$barcodeScan->target_type->name); - } - } -} diff --git a/templates/label_system/scanner/scanner.html.twig b/templates/label_system/scanner/scanner.html.twig index 39f4e140..1f978a9b 100644 --- a/templates/label_system/scanner/scanner.html.twig +++ b/templates/label_system/scanner/scanner.html.twig @@ -23,4 +23,22 @@ {{ form_end(form) }} + + {% if infoModeData %} +
+

{% trans %}label_scanner.decoded_info.title{% endtrans %}

+ + + + {% for key, value in infoModeData %} + + + + + {% endfor %} + +
{{ key }}{{ value }}
+ + {% endif %} + {% endblock %} diff --git a/templates/parts/edit/edit_form_styles.html.twig b/templates/parts/edit/edit_form_styles.html.twig index 6658fa80..cf9a40b9 100644 --- a/templates/parts/edit/edit_form_styles.html.twig +++ b/templates/parts/edit/edit_form_styles.html.twig @@ -108,7 +108,7 @@
{{ form_row(form.comment) }} {{ form_row(form.owner) }} - {{ form_row(form.vendor_barcode) }} + {{ form_row(form.user_barcode) }}
diff --git a/tests/Services/EntityMergers/Mergers/PartMergerTest.php b/tests/Services/EntityMergers/Mergers/PartMergerTest.php index fc60ca23..bf02744a 100644 --- a/tests/Services/EntityMergers/Mergers/PartMergerTest.php +++ b/tests/Services/EntityMergers/Mergers/PartMergerTest.php @@ -148,7 +148,7 @@ class PartMergerTest extends KernelTestCase public function testMergeOfPartLots(): void { $lot1 = (new PartLot())->setAmount(2)->setNeedsRefill(true); - $lot2 = (new PartLot())->setInstockUnknown(true)->setVendorBarcode('test'); + $lot2 = (new PartLot())->setInstockUnknown(true)->setUserBarcode('test'); $lot3 = (new PartLot())->setDescription('lot3')->setAmount(3); $lot4 = (new PartLot())->setDescription('lot4')->setComment('comment'); diff --git a/tests/Services/LabelSystem/Barcodes/BarcodeRedirectorTest.php b/tests/Services/LabelSystem/BarcodeScanner/BarcodeRedirectorTest.php similarity index 76% rename from tests/Services/LabelSystem/Barcodes/BarcodeRedirectorTest.php rename to tests/Services/LabelSystem/BarcodeScanner/BarcodeRedirectorTest.php index b2b94bab..58030f93 100644 --- a/tests/Services/LabelSystem/Barcodes/BarcodeRedirectorTest.php +++ b/tests/Services/LabelSystem/BarcodeScanner/BarcodeRedirectorTest.php @@ -39,12 +39,12 @@ declare(strict_types=1); * along with this program. If not, see . */ -namespace App\Tests\Services\LabelSystem\Barcodes; +namespace App\Tests\Services\LabelSystem\BarcodeScanner; use App\Entity\LabelSystem\LabelSupportedElement; -use App\Services\LabelSystem\Barcodes\BarcodeRedirector; -use App\Services\LabelSystem\Barcodes\BarcodeScanResult; -use App\Services\LabelSystem\Barcodes\BarcodeSourceType; +use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector; +use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType; +use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult; use Doctrine\ORM\EntityNotFoundException; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; @@ -60,17 +60,17 @@ final class BarcodeRedirectorTest extends KernelTestCase public static function urlDataProvider(): \Iterator { - yield [new BarcodeScanResult(LabelSupportedElement::PART, 1, BarcodeSourceType::INTERNAL), '/en/part/1']; + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 1, BarcodeSourceType::INTERNAL), '/en/part/1']; //Part lot redirects to Part info page (Part lot 1 is associated with part 3) - yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 1, BarcodeSourceType::INTERNAL), '/en/part/3']; - yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 1, BarcodeSourceType::INTERNAL), '/en/store_location/1/parts']; + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 1, BarcodeSourceType::INTERNAL), '/en/part/3']; + yield [new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 1, BarcodeSourceType::INTERNAL), '/en/store_location/1/parts']; } /** * @dataProvider urlDataProvider * @group DB */ - public function testGetRedirectURL(BarcodeScanResult $scanResult, string $url): void + public function testGetRedirectURL(LocalBarcodeScanResult $scanResult, string $url): void { $this->assertSame($url, $this->service->getRedirectURL($scanResult)); } @@ -79,7 +79,7 @@ final class BarcodeRedirectorTest extends KernelTestCase { $this->expectException(EntityNotFoundException::class); //If we encounter an invalid lot, we must throw an exception - $this->service->getRedirectURL(new BarcodeScanResult(LabelSupportedElement::PART_LOT, + $this->service->getRedirectURL(new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 12_345_678, BarcodeSourceType::INTERNAL)); } } diff --git a/tests/Services/LabelSystem/Barcodes/BarcodeScanHelperTest.php b/tests/Services/LabelSystem/BarcodeScanner/BarcodeScanHelperTest.php similarity index 60% rename from tests/Services/LabelSystem/Barcodes/BarcodeScanHelperTest.php rename to tests/Services/LabelSystem/BarcodeScanner/BarcodeScanHelperTest.php index 65cb02d4..a90a3bac 100644 --- a/tests/Services/LabelSystem/Barcodes/BarcodeScanHelperTest.php +++ b/tests/Services/LabelSystem/BarcodeScanner/BarcodeScanHelperTest.php @@ -39,13 +39,12 @@ declare(strict_types=1); * along with this program. If not, see . */ -namespace App\Tests\Services\LabelSystem\Barcodes; +namespace App\Tests\Services\LabelSystem\BarcodeScanner; use App\Entity\LabelSystem\LabelSupportedElement; -use App\Services\LabelSystem\Barcodes\BarcodeScanHelper; -use App\Services\LabelSystem\Barcodes\BarcodeScanResult; -use App\Services\LabelSystem\Barcodes\BarcodeSourceType; -use Com\Tecnick\Barcode\Barcode; +use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper; +use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType; +use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class BarcodeScanHelperTest extends WebTestCase @@ -61,55 +60,55 @@ class BarcodeScanHelperTest extends WebTestCase public static function dataProvider(): \Iterator { //QR URL content: - yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 1, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 1, BarcodeSourceType::INTERNAL), 'https://localhost:8000/scan/lot/1']; - yield [new BarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), 'https://localhost:8000/scan/part/123']; - yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 4, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 4, BarcodeSourceType::INTERNAL), 'http://foo.bar/part-db/scan/location/4']; //Current Code39 format: - yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), 'L0010']; - yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 123, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 123, BarcodeSourceType::INTERNAL), 'L0123']; - yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 123456, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 123456, BarcodeSourceType::INTERNAL), 'L123456']; - yield [new BarcodeScanResult(LabelSupportedElement::PART, 2, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 2, BarcodeSourceType::INTERNAL), 'P0002']; //Development phase Code39 barcodes: - yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), 'L-000010']; - yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), 'Lß000010']; - yield [new BarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), 'P-000123']; - yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 123, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 123, BarcodeSourceType::INTERNAL), 'S-000123']; - yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 12_345_678, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 12_345_678, BarcodeSourceType::INTERNAL), 'L-12345678']; //Legacy storelocation format - yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 336, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 336, BarcodeSourceType::INTERNAL), '$L00336']; - yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 12_345_678, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 12_345_678, BarcodeSourceType::INTERNAL), '$L12345678']; //Legacy Part format - yield [new BarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), '0000123']; - yield [new BarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), '00001236']; - yield [new BarcodeScanResult(LabelSupportedElement::PART, 1_234_567, BarcodeSourceType::INTERNAL), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 1_234_567, BarcodeSourceType::INTERNAL), '12345678']; //Test IPN barcode - yield [new BarcodeScanResult(LabelSupportedElement::PART, 2, BarcodeSourceType::IPN), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 2, BarcodeSourceType::IPN), 'IPN123']; //Test vendor barcode - yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 2,BarcodeSourceType::VENDOR), + yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 2,BarcodeSourceType::USER_DEFINED), 'lot2_vendor_barcode']; } @@ -131,7 +130,7 @@ class BarcodeScanHelperTest extends WebTestCase /** * @dataProvider dataProvider */ - public function testNormalizeBarcodeContent(BarcodeScanResult $expected, string $input): void + public function testNormalizeBarcodeContent(LocalBarcodeScanResult $expected, string $input): void { $this->assertEquals($expected, $this->service->scanBarcodeContent($input)); } diff --git a/tests/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResultTest.php b/tests/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResultTest.php new file mode 100644 index 00000000..aa5e4fbc --- /dev/null +++ b/tests/Services/LabelSystem/BarcodeScanner/EIGP114BarcodeScanResultTest.php @@ -0,0 +1,154 @@ +. + */ + +namespace App\Tests\Services\LabelSystem\BarcodeScanner; + +use App\Services\LabelSystem\BarcodeScanner\EIGP114BarcodeScanResult; +use PHPUnit\Framework\TestCase; + +class EIGP114BarcodeScanResultTest extends TestCase +{ + + public function testGuessBarcodeVendor(): void + { + //Generic barcode: + + $barcode = new EIGP114BarcodeScanResult([ + 'P' => '596-777A1-ND', + '1P' => 'XAF4444', + 'Q' => '3', + '10D' => '1452', + '1T' => 'BF1103', + '4L' => 'US', + ]); + + $this->assertNull($barcode->guessBarcodeVendor()); + + //Digikey barcode: + $barcode = new EIGP114BarcodeScanResult([ + 'P' => '596-777A1-ND', + '1P' => 'XAF4444', + 'Q' => '3', + '10D' => '1452', + '1T' => 'BF1103', + '4L' => 'US', + '13Z' => 'Digi-Key', + ]); + $this->assertEquals('digikey', $barcode->guessBarcodeVendor()); + + //Mouser barcode: + $barcode = new EIGP114BarcodeScanResult([ + 'P' => '596-777A1-ND', + '1P' => 'XAF4444', + 'Q' => '3', + '10D' => '1452', + '1T' => 'BF1103', + '4L' => 'US', + '1V' => 'Mouser', + ]); + + $this->assertEquals('mouser', $barcode->guessBarcodeVendor()); + + //Farnell barcode: + $barcode = new EIGP114BarcodeScanResult([ + 'P' => '596-777A1-ND', + '1P' => 'XAF4444', + 'Q' => '3', + '10D' => '1452', + '1T' => 'BF1103', + '4L' => 'US', + '3P' => 'Farnell', + ]); + + $this->assertEquals('element14', $barcode->guessBarcodeVendor()); + } + + public function testIsFormat06Code(): void + { + $this->assertFalse(EIGP114BarcodeScanResult::isFormat06Code('')); + $this->assertFalse(EIGP114BarcodeScanResult::isFormat06Code('test')); + $this->assertFalse(EIGP114BarcodeScanResult::isFormat06Code('12232435ew4rf')); + + //Valid code (with trailer) + $this->assertTrue(EIGP114BarcodeScanResult::isFormat06Code("[)>\x1E06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04")); + + //Valid code (digikey, without trailer) + $this->assertTrue(EIGP114BarcodeScanResult::isFormat06Code("[)>\x1e06\x1dPQ1045-ND\x1d1P364019-01\x1d30PQ1045-ND\x1dK12432 TRAVIS FOSS P\x1d1K85732873\x1d10K103332956\x1d9D231013\x1d1TQJ13P\x1d11K1\x1d4LTW\x1dQ3\x1d11ZPICK\x1d12Z7360988\x1d13Z999999\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000000000000000000000")); + } + + public function testParseFormat06CodeInvalid(): void + { + $this->expectException(\InvalidArgumentException::class); + EIGP114BarcodeScanResult::parseFormat06Code(''); + } + + public function testParseFormat06Code(): void + { + $barcode = EIGP114BarcodeScanResult::parseFormat06Code("[)>\x1E06\x1DP596-777A1-ND\x1D1PXAF4444\x1DQ3\x1D10D1452\x1D1TBF1103\x1D4LUS\x1E\x04"); + $this->assertEquals([ + 'P' => '596-777A1-ND', + '1P' => 'XAF4444', + 'Q' => '3', + '10D' => '1452', + '1T' => 'BF1103', + '4L' => 'US', + ], $barcode->data); + } + + public function testDataParsing(): void + { + $barcode = new EIGP114BarcodeScanResult([ + 'P' => '596-777A1-ND', + '1P' => 'XAF4444', + 'Q' => '3', + '10D' => '1452', + '1T' => 'BF1103', + '4L' => 'US', + ]); + + $this->assertEquals('596-777A1-ND', $barcode->customerPartNumber); + $this->assertEquals('XAF4444', $barcode->supplierPartNumber); + $this->assertEquals(3, $barcode->quantity); + $this->assertEquals('1452', $barcode->alternativeDateCode); + $this->assertEquals('BF1103', $barcode->lotCode); + $this->assertEquals('US', $barcode->countryOfOrigin); + } + + public function testDigikeyParsing(): void + { + $barcode = EIGP114BarcodeScanResult::parseFormat06Code("[)>\x1e06\x1dPQ1045-ND\x1d1P364019-01\x1d30PQ1045-ND\x1dK12432 TRAVIS FOSS P\x1d1K85732873\x1d10K103332956\x1d9D231013\x1d1TQJ13P\x1d11K1\x1d4LTW\x1dQ3\x1d11ZPICK\x1d12Z7360988\x1d13Z999999\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + + $this->assertEquals('digikey', $barcode->guessBarcodeVendor()); + + $this->assertEquals('Q1045-ND', $barcode->customerPartNumber); + $this->assertEquals('364019-01', $barcode->supplierPartNumber); + $this->assertEquals(3, $barcode->quantity); + $this->assertEquals('231013', $barcode->dateCode); + $this->assertEquals('QJ13P', $barcode->lotCode); + $this->assertEquals('TW', $barcode->countryOfOrigin); + $this->assertEquals('Q1045-ND', $barcode->digikeyPartNumber); + $this->assertEquals('85732873', $barcode->digikeySalesOrderNumber); + $this->assertEquals('103332956', $barcode->digikeyInvoiceNumber); + $this->assertEquals('PICK', $barcode->digikeyLabelType); + $this->assertEquals('7360988', $barcode->digikeyPartID); + $this->assertEquals('999999', $barcode->digikeyNA); + $this->assertEquals('0000000000000000000000000000000000000000000000000000000000000000000000000000000000000', $barcode->digikeyPadding); + } +} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 9c45b4be..8ca59f11 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12263,5 +12263,35 @@ Please note, that you can not impersonate a disabled user. If you try you will g Category could not be automatically determined by the info provider. Review the data and select the category manually. + + + part_lot.edit.user_barcode + User barcode + + + + + scan_dialog.mode.user + User defined barcode (configured at part lot) + + + + + scan_dialog.mode.eigp + EIGP 114 barcode (e.g. the datamatrix codes on digikey and mouser orders) + + + + + scan_dialog.info_mode + Info mode (Decode barcode and show its contents, but do not redirect to part) + + + + + label_scanner.decoded_info.title + Decoded information + +