Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 11 commits into from
Jan 4, 2025
Merged
27 changes: 18 additions & 9 deletions src/Controller/ScanController.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@
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;
Expand Down Expand Up @@ -77,13 +77,21 @@ public function dialog(Request $request, #[MapQueryParameter] ?string $input = n
$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');
Expand All @@ -92,6 +100,7 @@ public function dialog(Request $request, #[MapQueryParameter] ?string $input = n

return $this->render('label_system/scanner/scanner.html.twig', [
'form' => $form,
'infoModeData' => $infoModeData,
]);
}

Expand All @@ -109,7 +118,7 @@ public function scanQRCode(string $type, int $id): Response
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
Expand Down
2 changes: 1 addition & 1 deletion src/DataFixtures/PartFixtures.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public function load(ObjectManager $manager): void
$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();
Expand Down
16 changes: 8 additions & 8 deletions src/Entity/Parts/PartLot.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
#[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)'),
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -375,19 +375,19 @@ public function getName(): string
* 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;
}

Expand Down
12 changes: 10 additions & 2 deletions src/Form/LabelSystem/ScanDialogType.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@

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;
Expand All @@ -55,6 +56,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
{
$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',
Expand All @@ -71,9 +74,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
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, [
Expand Down
4 changes: 2 additions & 2 deletions src/Form/Part/PartLotType.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'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,
]);
Expand Down
5 changes: 2 additions & 3 deletions src/Services/InfoProviderSystem/DTOtoEntityConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,8 @@ public function convertPart(PartDetailDTO $dto, Part $entity = new Part()): Part

//Try to map the category to an existing entity (but never create a new one)
if ($dto->category) {
/** @var CategoryRepository<Category> $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));
Expand Down
166 changes: 166 additions & 0 deletions src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading
Loading