Skip to content

Commit 9e85b70

Browse files
Treeedjonajbtronics
authored
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 <[email protected]> Co-authored-by: Jan Böhmer <[email protected]>
1 parent 9c99217 commit 9e85b70

File tree

20 files changed

+868
-177
lines changed

20 files changed

+868
-177
lines changed

src/Controller/ScanController.php

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@
4242
namespace App\Controller;
4343

4444
use App\Form\LabelSystem\ScanDialogType;
45-
use App\Services\LabelSystem\Barcodes\BarcodeScanHelper;
46-
use App\Services\LabelSystem\Barcodes\BarcodeRedirector;
47-
use App\Services\LabelSystem\Barcodes\BarcodeScanResult;
48-
use App\Services\LabelSystem\Barcodes\BarcodeSourceType;
45+
use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector;
46+
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper;
47+
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
48+
use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
4949
use Doctrine\ORM\EntityNotFoundException;
5050
use InvalidArgumentException;
5151
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -77,13 +77,21 @@ public function dialog(Request $request, #[MapQueryParameter] ?string $input = n
7777
$mode = $form['mode']->getData();
7878
}
7979

80+
$infoModeData = null;
81+
8082
if ($input !== null) {
8183
try {
8284
$scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
83-
try {
84-
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
85-
} catch (EntityNotFoundException) {
86-
$this->addFlash('success', 'scan.qr_not_found');
85+
//Perform a redirect if the info mode is not enabled
86+
if (!$form['info_mode']->getData()) {
87+
try {
88+
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
89+
} catch (EntityNotFoundException) {
90+
$this->addFlash('success', 'scan.qr_not_found');
91+
}
92+
} else { //Otherwise retrieve infoModeData
93+
$infoModeData = $scan_result->getDecodedForInfoMode();
94+
8795
}
8896
} catch (InvalidArgumentException) {
8997
$this->addFlash('error', 'scan.format_unknown');
@@ -92,6 +100,7 @@ public function dialog(Request $request, #[MapQueryParameter] ?string $input = n
92100

93101
return $this->render('label_system/scanner/scanner.html.twig', [
94102
'form' => $form,
103+
'infoModeData' => $infoModeData,
95104
]);
96105
}
97106

@@ -109,7 +118,7 @@ public function scanQRCode(string $type, int $id): Response
109118
throw new InvalidArgumentException('Unknown type: '.$type);
110119
}
111120
//Construct the scan result manually, as we don't have a barcode here
112-
$scan_result = new BarcodeScanResult(
121+
$scan_result = new LocalBarcodeScanResult(
113122
target_type: BarcodeScanHelper::QR_TYPE_MAP[$type],
114123
target_id: $id,
115124
//The routes are only used on the internal generated QR codes

src/DataFixtures/PartFixtures.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public function load(ObjectManager $manager): void
106106
$partLot2->setComment('Test');
107107
$partLot2->setNeedsRefill(true);
108108
$partLot2->setStorageLocation($manager->find(StorageLocation::class, 3));
109-
$partLot2->setVendorBarcode('lot2_vendor_barcode');
109+
$partLot2->setUserBarcode('lot2_vendor_barcode');
110110
$part->addPartLot($partLot2);
111111

112112
$orderdetail = new Orderdetail();

src/Entity/Parts/PartLot.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
#[ORM\Index(columns: ['needs_refill'], name: 'part_lots_idx_needs_refill')]
6969
#[ORM\Index(columns: ['vendor_barcode'], name: 'part_lots_idx_barcode')]
7070
#[ValidPartLot]
71-
#[UniqueEntity(['vendor_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
71+
#[UniqueEntity(['user_barcode'], message: 'validator.part_lot.vendor_barcode_must_be_unique')]
7272
#[ApiResource(
7373
operations: [
7474
new Get(security: 'is_granted("read", object)'),
@@ -166,10 +166,10 @@ class PartLot extends AbstractDBElement implements TimeStampableInterface, Named
166166
/**
167167
* @var string|null The content of the barcode of this part lot (e.g. a barcode on the package put by the vendor)
168168
*/
169-
#[ORM\Column(type: Types::STRING, nullable: true)]
169+
#[ORM\Column(name: "vendor_barcode", type: Types::STRING, nullable: true)]
170170
#[Groups(['part_lot:read', 'part_lot:write'])]
171171
#[Length(max: 255)]
172-
protected ?string $vendor_barcode = null;
172+
protected ?string $user_barcode = null;
173173

174174
public function __clone()
175175
{
@@ -375,19 +375,19 @@ public function getName(): string
375375
* null if no barcode is set.
376376
* @return string|null
377377
*/
378-
public function getVendorBarcode(): ?string
378+
public function getUserBarcode(): ?string
379379
{
380-
return $this->vendor_barcode;
380+
return $this->user_barcode;
381381
}
382382

383383
/**
384384
* Set the content of the barcode of this part lot (e.g. a barcode on the package put by the vendor).
385-
* @param string|null $vendor_barcode
385+
* @param string|null $user_barcode
386386
* @return $this
387387
*/
388-
public function setVendorBarcode(?string $vendor_barcode): PartLot
388+
public function setUserBarcode(?string $user_barcode): PartLot
389389
{
390-
$this->vendor_barcode = $vendor_barcode;
390+
$this->user_barcode = $user_barcode;
391391
return $this;
392392
}
393393

src/Form/LabelSystem/ScanDialogType.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@
4141

4242
namespace App\Form\LabelSystem;
4343

44-
use App\Services\LabelSystem\Barcodes\BarcodeSourceType;
44+
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
4545
use Symfony\Component\Form\AbstractType;
46+
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
4647
use Symfony\Component\Form\Extension\Core\Type\EnumType;
4748
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
4849
use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -55,6 +56,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
5556
{
5657
$builder->add('input', TextType::class, [
5758
'label' => 'scan_dialog.input',
59+
//Do not trim the input, otherwise this damages Format06 barcodes which end with non-printable characters
60+
'trim' => false,
5861
'attr' => [
5962
'autofocus' => true,
6063
'id' => 'scan_dialog_input',
@@ -71,9 +74,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
7174
null => 'scan_dialog.mode.auto',
7275
BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal',
7376
BarcodeSourceType::IPN => 'scan_dialog.mode.ipn',
74-
BarcodeSourceType::VENDOR => 'scan_dialog.mode.vendor',
77+
BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user',
78+
BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp'
7579
},
80+
]);
7681

82+
$builder->add('info_mode', CheckboxType::class, [
83+
'label' => 'scan_dialog.info_mode',
84+
'required' => false,
7785
]);
7886

7987
$builder->add('submit', SubmitType::class, [

src/Form/Part/PartLotType.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
103103
'help' => 'part_lot.owner.help',
104104
]);
105105

106-
$builder->add('vendor_barcode', TextType::class, [
107-
'label' => 'part_lot.edit.vendor_barcode',
106+
$builder->add('user_barcode', TextType::class, [
107+
'label' => 'part_lot.edit.user_barcode',
108108
'help' => 'part_lot.edit.vendor_barcode.help',
109109
'required' => false,
110110
]);

src/Services/InfoProviderSystem/DTOtoEntityConverter.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,8 @@ public function convertPart(PartDetailDTO $dto, Part $entity = new Part()): Part
160160

161161
//Try to map the category to an existing entity (but never create a new one)
162162
if ($dto->category) {
163-
/** @var CategoryRepository<Category> $categoryRepo */
164-
$categoryRepo = $this->em->getRepository(Category::class);
165-
$entity->setCategory($categoryRepo->findForInfoProvider($dto->category));
163+
//@phpstan-ignore-next-line For some reason php does not recognize the repo returns a category
164+
$entity->setCategory($this->em->getRepository(Category::class)->findForInfoProvider($dto->category));
166165
}
167166

168167
$entity->setManufacturer($this->getOrCreateEntity(Manufacturer::class, $dto->manufacturer));
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
/**
24+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
25+
*
26+
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
27+
*
28+
* This program is free software: you can redistribute it and/or modify
29+
* it under the terms of the GNU Affero General Public License as published
30+
* by the Free Software Foundation, either version 3 of the License, or
31+
* (at your option) any later version.
32+
*
33+
* This program is distributed in the hope that it will be useful,
34+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
35+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36+
* GNU Affero General Public License for more details.
37+
*
38+
* You should have received a copy of the GNU Affero General Public License
39+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
40+
*/
41+
42+
namespace App\Services\LabelSystem\BarcodeScanner;
43+
44+
use App\Entity\LabelSystem\LabelSupportedElement;
45+
use App\Entity\Parts\Manufacturer;
46+
use App\Entity\Parts\Part;
47+
use App\Entity\Parts\PartLot;
48+
use Doctrine\ORM\EntityManagerInterface;
49+
use Doctrine\ORM\EntityNotFoundException;
50+
use InvalidArgumentException;
51+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
52+
53+
/**
54+
* @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeRedirectorTest
55+
*/
56+
final class BarcodeRedirector
57+
{
58+
public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em)
59+
{
60+
}
61+
62+
/**
63+
* Determines the URL to which the user should be redirected, when scanning a QR code.
64+
*
65+
* @param BarcodeScanResultInterface $barcodeScan The result of the barcode scan
66+
* @return string the URL to which should be redirected
67+
*
68+
* @throws EntityNotFoundException
69+
*/
70+
public function getRedirectURL(BarcodeScanResultInterface $barcodeScan): string
71+
{
72+
if($barcodeScan instanceof LocalBarcodeScanResult) {
73+
return $this->getURLLocalBarcode($barcodeScan);
74+
}
75+
76+
if ($barcodeScan instanceof EIGP114BarcodeScanResult) {
77+
return $this->getURLVendorBarcode($barcodeScan);
78+
}
79+
80+
throw new InvalidArgumentException('Unknown $barcodeScan type: '.get_class($barcodeScan));
81+
}
82+
83+
private function getURLLocalBarcode(LocalBarcodeScanResult $barcodeScan): string
84+
{
85+
switch ($barcodeScan->target_type) {
86+
case LabelSupportedElement::PART:
87+
return $this->urlGenerator->generate('app_part_show', ['id' => $barcodeScan->target_id]);
88+
case LabelSupportedElement::PART_LOT:
89+
//Try to determine the part to the given lot
90+
$lot = $this->em->find(PartLot::class, $barcodeScan->target_id);
91+
if (!$lot instanceof PartLot) {
92+
throw new EntityNotFoundException();
93+
}
94+
95+
return $this->urlGenerator->generate('app_part_show', ['id' => $lot->getPart()->getID()]);
96+
97+
case LabelSupportedElement::STORELOCATION:
98+
return $this->urlGenerator->generate('part_list_store_location', ['id' => $barcodeScan->target_id]);
99+
100+
default:
101+
throw new InvalidArgumentException('Unknown $type: '.$barcodeScan->target_type->name);
102+
}
103+
}
104+
105+
/**
106+
* Gets the URL to a part from a scan of a Vendor Barcode
107+
*/
108+
private function getURLVendorBarcode(EIGP114BarcodeScanResult $barcodeScan): string
109+
{
110+
$part = $this->getPartFromVendor($barcodeScan);
111+
return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]);
112+
}
113+
114+
/**
115+
* Gets a part from a scan of a Vendor Barcode by filtering for parts
116+
* with the same Info Provider Id or, if that fails, by looking for parts with a
117+
* matching manufacturer product number. Only returns the first matching part.
118+
*/
119+
private function getPartFromVendor(EIGP114BarcodeScanResult $barcodeScan) : Part
120+
{
121+
// first check via the info provider ID (e.g. Vendor ID). This might fail if the part was not added via
122+
// the info provider system or if the part was bought from a different vendor than the data was retrieved
123+
// from.
124+
if($barcodeScan->digikeyPartNumber) {
125+
$qb = $this->em->getRepository(Part::class)->createQueryBuilder('part');
126+
//Lower() to be case insensitive
127+
$qb->where($qb->expr()->like('LOWER(part.providerReference.provider_id)', 'LOWER(:vendor_id)'));
128+
$qb->setParameter('vendor_id', $barcodeScan->digikeyPartNumber);
129+
$results = $qb->getQuery()->getResult();
130+
if ($results) {
131+
return $results[0];
132+
}
133+
}
134+
135+
if(!$barcodeScan->supplierPartNumber){
136+
throw new EntityNotFoundException();
137+
}
138+
139+
//Fallback to the manufacturer part number. This may return false positives, since it is common for
140+
//multiple manufacturers to use the same part number for their version of a common product
141+
//We assume the user is able to realize when this returns the wrong part
142+
//If the barcode specifies the manufacturer we try to use that as well
143+
$mpnQb = $this->em->getRepository(Part::class)->createQueryBuilder('part');
144+
$mpnQb->where($mpnQb->expr()->like('LOWER(part.manufacturer_product_number)', 'LOWER(:mpn)'));
145+
$mpnQb->setParameter('mpn', $barcodeScan->supplierPartNumber);
146+
147+
if($barcodeScan->mouserManufacturer){
148+
$manufacturerQb = $this->em->getRepository(Manufacturer::class)->createQueryBuilder("manufacturer");
149+
$manufacturerQb->where($manufacturerQb->expr()->like("LOWER(manufacturer.name)", "LOWER(:manufacturer_name)"));
150+
$manufacturerQb->setParameter("manufacturer_name", $barcodeScan->mouserManufacturer);
151+
$manufacturers = $manufacturerQb->getQuery()->getResult();
152+
153+
if($manufacturers) {
154+
$mpnQb->andWhere($mpnQb->expr()->eq("part.manufacturer", ":manufacturer"));
155+
$mpnQb->setParameter("manufacturer", $manufacturers);
156+
}
157+
158+
}
159+
160+
$results = $mpnQb->getQuery()->getResult();
161+
if($results){
162+
return $results[0];
163+
}
164+
throw new EntityNotFoundException();
165+
}
166+
}

0 commit comments

Comments
 (0)