Added capability to scan Digikey barcodes and open the local part part page based on the result (#811)

* added capability to scan digikey barcodes and open the local part page based on the digikey part number or manufacturer part number

* had replaced one too many doublequotes

* Generalized interpretation of format06 barcodes, added ids for mouser

* Renamed vendor_barcode to user_barcode in entities

* Added a own class to parse EIGP114 barcodes

* Added tests to EIGP114Barcode parser

* Refactored code

* Changed BarcodeRedirector to support the new Barcode EIGP114BarcodeScanResult class

* Added possibility to just show all information contained in a barcode

* Dont require trailer for EIGP114 barcodes, as digikey does not seem to put them onto their  barcodes

* Fixed inspection issues

---------

Co-authored-by: jona <a@b.c>
Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
This commit is contained in:
Treeed 2025-01-04 01:20:51 +01:00 committed by GitHub
parent 9c99217dee
commit 9e85b70c17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 868 additions and 177 deletions

View file

@ -42,10 +42,10 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\Form\LabelSystem\ScanDialogType; use App\Form\LabelSystem\ScanDialogType;
use App\Services\LabelSystem\Barcodes\BarcodeScanHelper; use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector;
use App\Services\LabelSystem\Barcodes\BarcodeRedirector; use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper;
use App\Services\LabelSystem\Barcodes\BarcodeScanResult; use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
use App\Services\LabelSystem\Barcodes\BarcodeSourceType; use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
use Doctrine\ORM\EntityNotFoundException; use Doctrine\ORM\EntityNotFoundException;
use InvalidArgumentException; use InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -77,14 +77,22 @@ class ScanController extends AbstractController
$mode = $form['mode']->getData(); $mode = $form['mode']->getData();
} }
$infoModeData = null;
if ($input !== null) { if ($input !== null) {
try { try {
$scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null); $scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
//Perform a redirect if the info mode is not enabled
if (!$form['info_mode']->getData()) {
try { try {
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result)); return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
} catch (EntityNotFoundException) { } catch (EntityNotFoundException) {
$this->addFlash('success', 'scan.qr_not_found'); $this->addFlash('success', 'scan.qr_not_found');
} }
} else { //Otherwise retrieve infoModeData
$infoModeData = $scan_result->getDecodedForInfoMode();
}
} catch (InvalidArgumentException) { } catch (InvalidArgumentException) {
$this->addFlash('error', 'scan.format_unknown'); $this->addFlash('error', 'scan.format_unknown');
} }
@ -92,6 +100,7 @@ class ScanController extends AbstractController
return $this->render('label_system/scanner/scanner.html.twig', [ return $this->render('label_system/scanner/scanner.html.twig', [
'form' => $form, 'form' => $form,
'infoModeData' => $infoModeData,
]); ]);
} }
@ -109,7 +118,7 @@ class ScanController extends AbstractController
throw new InvalidArgumentException('Unknown type: '.$type); throw new InvalidArgumentException('Unknown type: '.$type);
} }
//Construct the scan result manually, as we don't have a barcode here //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_type: BarcodeScanHelper::QR_TYPE_MAP[$type],
target_id: $id, target_id: $id,
//The routes are only used on the internal generated QR codes //The routes are only used on the internal generated QR codes

View file

@ -106,7 +106,7 @@ class PartFixtures extends Fixture implements DependentFixtureInterface
$partLot2->setComment('Test'); $partLot2->setComment('Test');
$partLot2->setNeedsRefill(true); $partLot2->setNeedsRefill(true);
$partLot2->setStorageLocation($manager->find(StorageLocation::class, 3)); $partLot2->setStorageLocation($manager->find(StorageLocation::class, 3));
$partLot2->setVendorBarcode('lot2_vendor_barcode'); $partLot2->setUserBarcode('lot2_vendor_barcode');
$part->addPartLot($partLot2); $part->addPartLot($partLot2);
$orderdetail = new Orderdetail(); $orderdetail = new Orderdetail();

View file

@ -68,7 +68,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
#[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')] #[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')]
#[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')] #[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')]
#[ValidPartLot] #[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( #[ApiResource(
operations: [ operations: [
new Get(security: 'is_granted("read", object)'), 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) * @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'])] #[Groups(['part_lot:read', 'part_lot:write'])]
#[Length(max: 255)] #[Length(max: 255)]
protected ?string $vendor_barcode = null; protected ?string $user_barcode = null;
public function __clone() public function __clone()
{ {
@ -375,19 +375,19 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
* null if no barcode is set. * null if no barcode is set.
* @return string|null * @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). * 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 * @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; return $this;
} }

View file

@ -41,8 +41,9 @@ declare(strict_types=1);
namespace App\Form\LabelSystem; 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\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
@ -55,6 +56,8 @@ class ScanDialogType extends AbstractType
{ {
$builder->add('input', TextType::class, [ $builder->add('input', TextType::class, [
'label' => 'scan_dialog.input', 'label' => 'scan_dialog.input',
//Do not trim the input, otherwise this damages Format06 barcodes which end with non-printable characters
'trim' => false,
'attr' => [ 'attr' => [
'autofocus' => true, 'autofocus' => true,
'id' => 'scan_dialog_input', 'id' => 'scan_dialog_input',
@ -71,9 +74,14 @@ class ScanDialogType extends AbstractType
null => 'scan_dialog.mode.auto', null => 'scan_dialog.mode.auto',
BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal', BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal',
BarcodeSourceType::IPN => 'scan_dialog.mode.ipn', 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, [ $builder->add('submit', SubmitType::class, [

View file

@ -103,8 +103,8 @@ class PartLotType extends AbstractType
'help' => 'part_lot.owner.help', 'help' => 'part_lot.owner.help',
]); ]);
$builder->add('vendor_barcode', TextType::class, [ $builder->add('user_barcode', TextType::class, [
'label' => 'part_lot.edit.vendor_barcode', 'label' => 'part_lot.edit.user_barcode',
'help' => 'part_lot.edit.vendor_barcode.help', 'help' => 'part_lot.edit.vendor_barcode.help',
'required' => false, 'required' => false,
]); ]);

View file

@ -160,9 +160,8 @@ final class DTOtoEntityConverter
//Try to map the category to an existing entity (but never create a new one) //Try to map the category to an existing entity (but never create a new one)
if ($dto->category) { if ($dto->category) {
/** @var CategoryRepository<Category> $categoryRepo */ //@phpstan-ignore-next-line For some reason php does not recognize the repo returns a category
$categoryRepo = $this->em->getRepository(Category::class); $entity->setCategory($this->em->getRepository(Category::class)->findForInfoProvider($dto->category));
$entity->setCategory($categoryRepo->findForInfoProvider($dto->category));
} }
$entity->setManufacturer($this->getOrCreateEntity(Manufacturer::class, $dto->manufacturer)); $entity->setManufacturer($this->getOrCreateEntity(Manufacturer::class, $dto->manufacturer));

View file

@ -0,0 +1,166 @@
<?php
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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 <https://www.gnu.org/licenses/>.
*/
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();
}
}

View file

@ -39,7 +39,7 @@ declare(strict_types=1);
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
namespace App\Services\LabelSystem\Barcodes; namespace App\Services\LabelSystem\BarcodeScanner;
use App\Entity\LabelSystem\LabelSupportedElement; use App\Entity\LabelSystem\LabelSupportedElement;
use App\Entity\Parts\Part; use App\Entity\Parts\Part;
@ -75,30 +75,39 @@ final class BarcodeScanHelper
* will try to guess the type. * will try to guess the type.
* @param string $input * @param string $input
* @param BarcodeSourceType|null $type * @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 //Do specific parsing
if ($type === BarcodeSourceType::INTERNAL) { if ($type === BarcodeSourceType::INTERNAL) {
return $this->parseInternalBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); return $this->parseInternalBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
} }
if ($type === BarcodeSourceType::VENDOR) { if ($type === BarcodeSourceType::USER_DEFINED) {
return $this->parseVendorBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); return $this->parseUserDefinedBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
} }
if ($type === BarcodeSourceType::IPN) { if ($type === BarcodeSourceType::IPN) {
return $this->parseIPNBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); 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 //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); $result = $this->parseInternalBarcode($input);
if ($result !== null) { if ($result !== null) {
return $result; return $result;
} }
//Try to parse as vendor barcode //Try to parse as User defined barcode
$result = $this->parseVendorBarcode($input); $result = $this->parseUserDefinedBarcode($input);
if ($result !== null) { if ($result !== null) {
return $result; return $result;
} }
@ -112,11 +121,16 @@ final class BarcodeScanHelper
throw new InvalidArgumentException('Unknown barcode'); 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); $lot_repo = $this->entityManager->getRepository(PartLot::class);
//Find only the first result //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) { if (count($results) === 0) {
return null; return null;
@ -124,14 +138,14 @@ final class BarcodeScanHelper
//We found a part, so use it to create the result //We found a part, so use it to create the result
$lot = $results[0]; $lot = $results[0];
return new BarcodeScanResult( return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::PART_LOT, target_type: LabelSupportedElement::PART_LOT,
target_id: $lot->getID(), 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); $part_repo = $this->entityManager->getRepository(Part::class);
//Find only the first result //Find only the first result
@ -143,7 +157,7 @@ final class BarcodeScanHelper
//We found a part, so use it to create the result //We found a part, so use it to create the result
$part = $results[0]; $part = $results[0];
return new BarcodeScanResult( return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::PART, target_type: LabelSupportedElement::PART,
target_id: $part->getID(), target_id: $part->getID(),
source_type: BarcodeSourceType::IPN 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 * 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. * not be found in the database, an exception is thrown.
* @param string $input * @param string $input
* @return BarcodeScanResult|null * @return LocalBarcodeScanResult|null
*/ */
private function parseInternalBarcode(string $input): ?BarcodeScanResult private function parseInternalBarcode(string $input): ?LocalBarcodeScanResult
{ {
$input = trim($input); $input = trim($input);
$matches = []; $matches = [];
@ -167,7 +181,7 @@ final class BarcodeScanHelper
//Extract parts from QR code's URL //Extract parts from QR code's URL
if (preg_match('#^https?://.*/scan/(\w+)/(\d+)/?$#', $input, $matches)) { 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_type: self::QR_TYPE_MAP[strtolower($matches[1])],
target_id: (int) $matches[2], target_id: (int) $matches[2],
source_type: BarcodeSourceType::INTERNAL source_type: BarcodeSourceType::INTERNAL
@ -183,7 +197,7 @@ final class BarcodeScanHelper
throw new InvalidArgumentException('Unknown prefix '.$prefix); throw new InvalidArgumentException('Unknown prefix '.$prefix);
} }
return new BarcodeScanResult( return new LocalBarcodeScanResult(
target_type: self::PREFIX_TYPE_MAP[$prefix], target_type: self::PREFIX_TYPE_MAP[$prefix],
target_id: $id, target_id: $id,
source_type: BarcodeSourceType::INTERNAL source_type: BarcodeSourceType::INTERNAL
@ -199,7 +213,7 @@ final class BarcodeScanHelper
throw new InvalidArgumentException('Unknown prefix '.$prefix); throw new InvalidArgumentException('Unknown prefix '.$prefix);
} }
return new BarcodeScanResult( return new LocalBarcodeScanResult(
target_type: self::PREFIX_TYPE_MAP[$prefix], target_type: self::PREFIX_TYPE_MAP[$prefix],
target_id: $id, target_id: $id,
source_type: BarcodeSourceType::INTERNAL source_type: BarcodeSourceType::INTERNAL
@ -208,7 +222,7 @@ final class BarcodeScanHelper
//Legacy Part-DB location labels used $L00336 format //Legacy Part-DB location labels used $L00336 format
if (preg_match('#^\$L(\d{5,})$#', $input, $matches)) { if (preg_match('#^\$L(\d{5,})$#', $input, $matches)) {
return new BarcodeScanResult( return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::STORELOCATION, target_type: LabelSupportedElement::STORELOCATION,
target_id: (int) $matches[1], target_id: (int) $matches[1],
source_type: BarcodeSourceType::INTERNAL 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) //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)) { if (preg_match('#^(\d{7})\d?$#', $input, $matches)) {
return new BarcodeScanResult( return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::PART, target_type: LabelSupportedElement::PART,
target_id: (int) $matches[1], target_id: (int) $matches[1],
source_type: BarcodeSourceType::INTERNAL source_type: BarcodeSourceType::INTERNAL

View file

@ -0,0 +1,36 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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\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<string, string|int|float|null>
*/
public function getDecodedForInfoMode(): array;
}

View file

@ -21,7 +21,7 @@
declare(strict_types=1); 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 * This enum represents the different types, where a barcode/QR-code can be generated from
@ -32,9 +32,14 @@ enum BarcodeSourceType
case INTERNAL; case INTERNAL;
/** This barcode is containing an internal part number (IPN) */ /** This barcode is containing an internal part number (IPN) */
case IPN; case IPN;
/** /**
* This barcode is a custom barcode from a third party like a vendor, which was set via the vendor_barcode * This barcode is a user defined barcode defined on a part lot
* field of a part lot.
*/ */
case VENDOR; case USER_DEFINED;
/**
* EIGP114 formatted barcodes like used by digikey, mouser, etc.
*/
case EIGP114;
} }

View file

@ -0,0 +1,332 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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\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<string, string> $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 [)><RS>06<GS>
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 <GS> 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;
}
}

View file

@ -21,14 +21,15 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Services\LabelSystem\Barcodes; namespace App\Services\LabelSystem\BarcodeScanner;
use App\Entity\LabelSystem\LabelSupportedElement; 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 function __construct(
public readonly LabelSupportedElement $target_type, public readonly LabelSupportedElement $target_type,
@ -36,4 +37,13 @@ class BarcodeScanResult
public readonly BarcodeSourceType $source_type, 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,
];
}
} }

View file

@ -1,89 +0,0 @@
<?php
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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 <https://www.gnu.org/licenses/>.
*/
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);
}
}
}

View file

@ -23,4 +23,22 @@
{{ form_end(form) }} {{ form_end(form) }}
{% if infoModeData %}
<hr>
<h4>{% trans %}label_scanner.decoded_info.title{% endtrans %}</h4>
<table class="table table-striped table-hover table-bordered table-sm">
<tbody>
{% for key, value in infoModeData %}
<tr>
<td>{{ key }}</td>
<td><code>{{ value }}</code></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -108,7 +108,7 @@
<div class="collapse" id="{{ id }}"> <div class="collapse" id="{{ id }}">
{{ form_row(form.comment) }} {{ form_row(form.comment) }}
{{ form_row(form.owner) }} {{ form_row(form.owner) }}
{{ form_row(form.vendor_barcode) }} {{ form_row(form.user_barcode) }}
</div> </div>
</td> </td>
<td> <td>

View file

@ -148,7 +148,7 @@ class PartMergerTest extends KernelTestCase
public function testMergeOfPartLots(): void public function testMergeOfPartLots(): void
{ {
$lot1 = (new PartLot())->setAmount(2)->setNeedsRefill(true); $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); $lot3 = (new PartLot())->setDescription('lot3')->setAmount(3);
$lot4 = (new PartLot())->setDescription('lot4')->setComment('comment'); $lot4 = (new PartLot())->setDescription('lot4')->setComment('comment');

View file

@ -39,12 +39,12 @@ declare(strict_types=1);
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
namespace App\Tests\Services\LabelSystem\Barcodes; namespace App\Tests\Services\LabelSystem\BarcodeScanner;
use App\Entity\LabelSystem\LabelSupportedElement; use App\Entity\LabelSystem\LabelSupportedElement;
use App\Services\LabelSystem\Barcodes\BarcodeRedirector; use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector;
use App\Services\LabelSystem\Barcodes\BarcodeScanResult; use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
use App\Services\LabelSystem\Barcodes\BarcodeSourceType; use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
use Doctrine\ORM\EntityNotFoundException; use Doctrine\ORM\EntityNotFoundException;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
@ -60,17 +60,17 @@ final class BarcodeRedirectorTest extends KernelTestCase
public static function urlDataProvider(): \Iterator 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) //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 LocalBarcodeScanResult(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::STORELOCATION, 1, BarcodeSourceType::INTERNAL), '/en/store_location/1/parts'];
} }
/** /**
* @dataProvider urlDataProvider * @dataProvider urlDataProvider
* @group DB * @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)); $this->assertSame($url, $this->service->getRedirectURL($scanResult));
} }
@ -79,7 +79,7 @@ final class BarcodeRedirectorTest extends KernelTestCase
{ {
$this->expectException(EntityNotFoundException::class); $this->expectException(EntityNotFoundException::class);
//If we encounter an invalid lot, we must throw an exception //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)); 12_345_678, BarcodeSourceType::INTERNAL));
} }
} }

View file

@ -39,13 +39,12 @@ declare(strict_types=1);
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
namespace App\Tests\Services\LabelSystem\Barcodes; namespace App\Tests\Services\LabelSystem\BarcodeScanner;
use App\Entity\LabelSystem\LabelSupportedElement; use App\Entity\LabelSystem\LabelSupportedElement;
use App\Services\LabelSystem\Barcodes\BarcodeScanHelper; use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper;
use App\Services\LabelSystem\Barcodes\BarcodeScanResult; use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
use App\Services\LabelSystem\Barcodes\BarcodeSourceType; use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
use Com\Tecnick\Barcode\Barcode;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class BarcodeScanHelperTest extends WebTestCase class BarcodeScanHelperTest extends WebTestCase
@ -61,55 +60,55 @@ class BarcodeScanHelperTest extends WebTestCase
public static function dataProvider(): \Iterator public static function dataProvider(): \Iterator
{ {
//QR URL content: //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']; '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']; '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']; 'http://foo.bar/part-db/scan/location/4'];
//Current Code39 format: //Current Code39 format:
yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL),
'L0010']; 'L0010'];
yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 123, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 123, BarcodeSourceType::INTERNAL),
'L0123']; 'L0123'];
yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 123456, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 123456, BarcodeSourceType::INTERNAL),
'L123456']; 'L123456'];
yield [new BarcodeScanResult(LabelSupportedElement::PART, 2, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 2, BarcodeSourceType::INTERNAL),
'P0002']; 'P0002'];
//Development phase Code39 barcodes: //Development phase Code39 barcodes:
yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL),
'L-000010']; 'L-000010'];
yield [new BarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::PART_LOT, 10, BarcodeSourceType::INTERNAL),
'Lß000010']; 'Lß000010'];
yield [new BarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL),
'P-000123']; 'P-000123'];
yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 123, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 123, BarcodeSourceType::INTERNAL),
'S-000123']; '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']; 'L-12345678'];
//Legacy storelocation format //Legacy storelocation format
yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 336, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 336, BarcodeSourceType::INTERNAL),
'$L00336']; '$L00336'];
yield [new BarcodeScanResult(LabelSupportedElement::STORELOCATION, 12_345_678, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::STORELOCATION, 12_345_678, BarcodeSourceType::INTERNAL),
'$L12345678']; '$L12345678'];
//Legacy Part format //Legacy Part format
yield [new BarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL),
'0000123']; '0000123'];
yield [new BarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 123, BarcodeSourceType::INTERNAL),
'00001236']; '00001236'];
yield [new BarcodeScanResult(LabelSupportedElement::PART, 1_234_567, BarcodeSourceType::INTERNAL), yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 1_234_567, BarcodeSourceType::INTERNAL),
'12345678']; '12345678'];
//Test IPN barcode //Test IPN barcode
yield [new BarcodeScanResult(LabelSupportedElement::PART, 2, BarcodeSourceType::IPN), yield [new LocalBarcodeScanResult(LabelSupportedElement::PART, 2, BarcodeSourceType::IPN),
'IPN123']; 'IPN123'];
//Test vendor barcode //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']; 'lot2_vendor_barcode'];
} }
@ -131,7 +130,7 @@ class BarcodeScanHelperTest extends WebTestCase
/** /**
* @dataProvider dataProvider * @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)); $this->assertEquals($expected, $this->service->scanBarcodeContent($input));
} }

View file

@ -0,0 +1,154 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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/>.
*/
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);
}
}

View file

@ -12263,5 +12263,35 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<target>Category could not be automatically determined by the info provider. Review the data and select the category manually.</target> <target>Category could not be automatically determined by the info provider. Review the data and select the category manually.</target>
</segment> </segment>
</unit> </unit>
<unit id="v6oyTac" name="part_lot.edit.user_barcode">
<segment>
<source>part_lot.edit.user_barcode</source>
<target>User barcode</target>
</segment>
</unit>
<unit id="dXhegcm" name="scan_dialog.mode.user">
<segment>
<source>scan_dialog.mode.user</source>
<target>User defined barcode (configured at part lot)</target>
</segment>
</unit>
<unit id="sSAJDdr" name="scan_dialog.mode.eigp">
<segment>
<source>scan_dialog.mode.eigp</source>
<target>EIGP 114 barcode (e.g. the datamatrix codes on digikey and mouser orders)</target>
</segment>
</unit>
<unit id="QSMS_Bd" name="scan_dialog.info_mode">
<segment>
<source>scan_dialog.info_mode</source>
<target>Info mode (Decode barcode and show its contents, but do not redirect to part)</target>
</segment>
</unit>
<unit id="k5Gvkgp" name="label_scanner.decoded_info.title">
<segment>
<source>label_scanner.decoded_info.title</source>
<target>Decoded information</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>