2020-04-24 22:10:49 +02:00
|
|
|
|
<?php
|
2022-11-29 21:21:26 +01:00
|
|
|
|
/*
|
|
|
|
|
|
* 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/>.
|
|
|
|
|
|
*/
|
2020-05-10 21:39:31 +02:00
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
2020-04-24 22:10:49 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
|
|
|
|
*
|
2022-11-29 22:28:53 +01:00
|
|
|
|
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
2020-04-24 22:10:49 +02:00
|
|
|
|
*
|
|
|
|
|
|
* 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\Controller;
|
|
|
|
|
|
|
2020-04-26 21:26:11 +02:00
|
|
|
|
use App\Form\LabelSystem\ScanDialogType;
|
2025-01-04 01:20:51 +01:00
|
|
|
|
use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector;
|
|
|
|
|
|
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper;
|
|
|
|
|
|
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
|
|
|
|
|
use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
|
2026-01-16 13:59:26 +13:00
|
|
|
|
use App\Services\LabelSystem\BarcodeScanner\LCSCBarcodeScanResult;
|
|
|
|
|
|
use App\Services\LabelSystem\BarcodeScanner\EIGP114BarcodeScanResult;
|
2020-04-26 18:59:49 +02:00
|
|
|
|
use Doctrine\ORM\EntityNotFoundException;
|
2022-08-14 19:32:53 +02:00
|
|
|
|
use InvalidArgumentException;
|
2020-04-24 22:10:49 +02:00
|
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
2020-04-26 21:26:11 +02:00
|
|
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
|
|
|
|
use Symfony\Component\HttpFoundation\Response;
|
2023-07-02 14:16:32 +02:00
|
|
|
|
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
|
2024-03-03 20:37:33 +01:00
|
|
|
|
use Symfony\Component\Routing\Attribute\Route;
|
2026-01-16 13:59:26 +13:00
|
|
|
|
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
|
|
|
|
|
use App\Services\InfoProviderSystem\ProviderRegistry;
|
2026-01-16 22:42:20 +13:00
|
|
|
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
|
|
|
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
|
|
|
|
|
use App\Entity\Parts\Part;
|
|
|
|
|
|
use \App\Entity\Parts\StorageLocation;
|
2020-04-24 22:10:49 +02:00
|
|
|
|
|
2024-06-22 00:31:43 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* @see \App\Tests\Controller\ScanControllerTest
|
|
|
|
|
|
*/
|
2023-05-28 01:21:05 +02:00
|
|
|
|
#[Route(path: '/scan')]
|
2020-04-24 22:10:49 +02:00
|
|
|
|
class ScanController extends AbstractController
|
|
|
|
|
|
{
|
2026-01-16 13:59:26 +13:00
|
|
|
|
public function __construct(
|
|
|
|
|
|
protected BarcodeRedirector $barcodeParser,
|
|
|
|
|
|
protected BarcodeScanHelper $barcodeNormalizer,
|
|
|
|
|
|
private readonly ProviderRegistry $providerRegistry,
|
|
|
|
|
|
private readonly PartInfoRetriever $infoRetriever,
|
|
|
|
|
|
) {}
|
2020-04-26 21:26:11 +02:00
|
|
|
|
|
2023-07-02 17:46:09 +02:00
|
|
|
|
#[Route(path: '', name: 'scan_dialog')]
|
2023-07-02 14:16:32 +02:00
|
|
|
|
public function dialog(Request $request, #[MapQueryParameter] ?string $input = null): Response
|
2020-04-26 21:26:11 +02:00
|
|
|
|
{
|
2020-05-08 12:50:44 +02:00
|
|
|
|
$this->denyAccessUnlessGranted('@tools.label_scanner');
|
|
|
|
|
|
|
2020-04-26 21:26:11 +02:00
|
|
|
|
$form = $this->createForm(ScanDialogType::class);
|
|
|
|
|
|
$form->handleRequest($request);
|
|
|
|
|
|
|
2026-01-16 13:59:26 +13:00
|
|
|
|
$mode = null;
|
2023-07-02 14:16:32 +02:00
|
|
|
|
if ($input === null && $form->isSubmitted() && $form->isValid()) {
|
2020-04-26 21:26:11 +02:00
|
|
|
|
$input = $form['input']->getData();
|
2023-11-12 00:36:13 +01:00
|
|
|
|
$mode = $form['mode']->getData();
|
2023-07-02 14:16:32 +02:00
|
|
|
|
}
|
2020-05-10 21:39:31 +02:00
|
|
|
|
|
2025-01-04 01:20:51 +01:00
|
|
|
|
$infoModeData = null;
|
2026-01-16 13:59:26 +13:00
|
|
|
|
$createUrl = null;
|
2025-01-04 01:20:51 +01:00
|
|
|
|
|
2023-07-02 14:16:32 +02:00
|
|
|
|
if ($input !== null) {
|
2020-04-26 21:26:11 +02:00
|
|
|
|
try {
|
2023-11-12 00:36:13 +01:00
|
|
|
|
$scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
|
2026-01-16 13:59:26 +13:00
|
|
|
|
|
2025-01-04 01:20:51 +01:00
|
|
|
|
//Perform a redirect if the info mode is not enabled
|
|
|
|
|
|
if (!$form['info_mode']->getData()) {
|
|
|
|
|
|
try {
|
2026-01-16 13:59:26 +13:00
|
|
|
|
// redirect user to part page
|
2025-01-04 01:20:51 +01:00
|
|
|
|
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
|
|
|
|
|
|
} catch (EntityNotFoundException) {
|
2026-01-16 22:42:20 +13:00
|
|
|
|
// Part not found -> show decoded info + optional "create part" link
|
2026-01-16 13:59:26 +13:00
|
|
|
|
$infoModeData = $scan_result->getDecodedForInfoMode();
|
|
|
|
|
|
|
2026-01-16 22:42:20 +13:00
|
|
|
|
$createUrl = $this->buildCreateUrlForScanResult($scan_result, $request->getLocale());
|
2026-01-16 13:59:26 +13:00
|
|
|
|
|
|
|
|
|
|
if ($createUrl === null) {
|
|
|
|
|
|
$this->addFlash('warning', 'scan.qr_not_found');
|
|
|
|
|
|
}
|
2025-01-04 01:20:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
} else { //Otherwise retrieve infoModeData
|
|
|
|
|
|
$infoModeData = $scan_result->getDecodedForInfoMode();
|
2020-04-26 21:26:11 +02:00
|
|
|
|
}
|
2023-06-11 14:15:46 +02:00
|
|
|
|
} catch (InvalidArgumentException) {
|
2020-04-26 21:26:11 +02:00
|
|
|
|
$this->addFlash('error', 'scan.format_unknown');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-05-28 01:21:05 +02:00
|
|
|
|
return $this->render('label_system/scanner/scanner.html.twig', [
|
2022-03-04 21:20:18 +01:00
|
|
|
|
'form' => $form,
|
2025-01-04 01:20:51 +01:00
|
|
|
|
'infoModeData' => $infoModeData,
|
2026-01-16 13:59:26 +13:00
|
|
|
|
'createUrl' => $createUrl,
|
2020-04-26 21:26:11 +02:00
|
|
|
|
]);
|
2020-04-26 18:59:49 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-04-24 22:10:49 +02:00
|
|
|
|
/**
|
2020-05-10 21:39:31 +02:00
|
|
|
|
* The route definition for this action is done in routes.yaml, as it does not use the _locale prefix as the other routes.
|
2020-04-24 22:10:49 +02:00
|
|
|
|
*/
|
2020-04-26 21:26:11 +02:00
|
|
|
|
public function scanQRCode(string $type, int $id): Response
|
2020-04-24 22:10:49 +02:00
|
|
|
|
{
|
2023-10-26 22:23:43 +02:00
|
|
|
|
$type = strtolower($type);
|
|
|
|
|
|
|
2020-04-26 18:59:49 +02:00
|
|
|
|
try {
|
|
|
|
|
|
$this->addFlash('success', 'scan.qr_success');
|
2020-05-10 21:39:31 +02:00
|
|
|
|
|
2023-10-26 22:23:43 +02:00
|
|
|
|
if (!isset(BarcodeScanHelper::QR_TYPE_MAP[$type])) {
|
|
|
|
|
|
throw new InvalidArgumentException('Unknown type: '.$type);
|
|
|
|
|
|
}
|
|
|
|
|
|
//Construct the scan result manually, as we don't have a barcode here
|
2025-01-04 01:20:51 +01:00
|
|
|
|
$scan_result = new LocalBarcodeScanResult(
|
2023-10-26 22:23:43 +02:00
|
|
|
|
target_type: BarcodeScanHelper::QR_TYPE_MAP[$type],
|
|
|
|
|
|
target_id: $id,
|
|
|
|
|
|
//The routes are only used on the internal generated QR codes
|
|
|
|
|
|
source_type: BarcodeSourceType::INTERNAL
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
|
2023-06-11 14:15:46 +02:00
|
|
|
|
} catch (EntityNotFoundException) {
|
2020-04-26 18:59:49 +02:00
|
|
|
|
$this->addFlash('success', 'scan.qr_not_found');
|
2020-05-10 21:39:31 +02:00
|
|
|
|
|
2020-04-26 18:59:49 +02:00
|
|
|
|
return $this->redirectToRoute('homepage');
|
|
|
|
|
|
}
|
2020-04-24 22:10:49 +02:00
|
|
|
|
}
|
2026-01-16 22:42:20 +13:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Builds a URL for creating a new part based on the barcode data
|
|
|
|
|
|
* @param object $scanResult
|
|
|
|
|
|
* @param string $locale
|
|
|
|
|
|
* @return string|null
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function buildCreateUrlForScanResult(object $scanResult, string $locale): ?string
|
|
|
|
|
|
{
|
|
|
|
|
|
// LCSC
|
|
|
|
|
|
if ($scanResult instanceof LCSCBarcodeScanResult) {
|
|
|
|
|
|
$lcscCode = $scanResult->getPC();
|
|
|
|
|
|
if (is_string($lcscCode) && $lcscCode !== '') {
|
|
|
|
|
|
return '/'
|
|
|
|
|
|
. rawurlencode($locale)
|
|
|
|
|
|
. '/part/from_info_provider/lcsc/'
|
|
|
|
|
|
. rawurlencode($lcscCode)
|
|
|
|
|
|
. '/create';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Mouser / Digi-Key (EIGP114)
|
|
|
|
|
|
if ($scanResult instanceof EIGP114BarcodeScanResult) {
|
|
|
|
|
|
$vendor = $scanResult->guessBarcodeVendor();
|
|
|
|
|
|
|
|
|
|
|
|
// Mouser: use supplierPartNumber -> search provider -> provider_id
|
|
|
|
|
|
if ($vendor === 'mouser'
|
|
|
|
|
|
&& is_string($scanResult->supplierPartNumber)
|
|
|
|
|
|
&& $scanResult->supplierPartNumber !== ''
|
|
|
|
|
|
) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
$mouserProvider = $this->providerRegistry->getProviderByKey('mouser');
|
|
|
|
|
|
|
|
|
|
|
|
if (!$mouserProvider->isActive()) {
|
|
|
|
|
|
$this->addFlash('warning', 'Mouser provider is disabled / not configured.');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Search Mouser using the MPN
|
|
|
|
|
|
$dtos = $this->infoRetriever->searchByKeyword(
|
|
|
|
|
|
keyword: $scanResult->supplierPartNumber,
|
|
|
|
|
|
providers: [$mouserProvider]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// If there are results, provider_id is MouserPartNumber (per MouserProvider.php)
|
|
|
|
|
|
$best = $dtos[0] ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
if ($best !== null && is_string($best->provider_id) && $best->provider_id !== '') {
|
|
|
|
|
|
return '/'
|
|
|
|
|
|
. rawurlencode($locale)
|
|
|
|
|
|
. '/part/from_info_provider/mouser/'
|
|
|
|
|
|
. rawurlencode($best->provider_id)
|
|
|
|
|
|
. '/create';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$this->addFlash('warning', 'No Mouser match found for this MPN.');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
} catch (\InvalidArgumentException) {
|
|
|
|
|
|
// provider key not found in registry
|
|
|
|
|
|
$this->addFlash('warning', 'Mouser provider is not installed/enabled.');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
|
// Don’t break scanning UX if provider lookup fails
|
|
|
|
|
|
$this->addFlash('warning', 'Mouser lookup failed: ' . $e->getMessage());
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Digi-Key: can use customerPartNumber or supplierPartNumber directly
|
|
|
|
|
|
if ($vendor === 'digikey') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
$provider = $this->providerRegistry->getProviderByKey('digikey');
|
|
|
|
|
|
|
|
|
|
|
|
if (!$provider->isActive()) {
|
|
|
|
|
|
$this->addFlash('warning', 'Digi-Key provider is disabled / not configured (API key missing).');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$id = $scanResult->customerPartNumber ?: $scanResult->supplierPartNumber;
|
|
|
|
|
|
|
|
|
|
|
|
if (is_string($id) && $id !== '') {
|
|
|
|
|
|
return '/'
|
|
|
|
|
|
. rawurlencode($locale)
|
|
|
|
|
|
. '/part/from_info_provider/digikey/'
|
|
|
|
|
|
. rawurlencode($id)
|
|
|
|
|
|
. '/create';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (\InvalidArgumentException) {
|
|
|
|
|
|
$this->addFlash('warning', 'Digi-Key provider is not installed/enabled');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function buildLocationsForPart(Part $part): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$byLocationId = [];
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($part->getPartLots() as $lot) {
|
|
|
|
|
|
$loc = $lot->getStorageLocation();
|
|
|
|
|
|
if ($loc === null) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$locId = $loc->getID();
|
|
|
|
|
|
$qty = $lot->getAmount();
|
|
|
|
|
|
|
|
|
|
|
|
if (!isset($byLocationId[$locId])) {
|
|
|
|
|
|
$byLocationId[$locId] = [
|
|
|
|
|
|
'breadcrumb' => $this->buildStorageBreadcrumb($loc),
|
|
|
|
|
|
'qty' => $qty,
|
|
|
|
|
|
];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$byLocationId[$locId]['qty'] += $qty;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return array_values($byLocationId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function buildStorageBreadcrumb(StorageLocation $loc): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$items = [];
|
|
|
|
|
|
$cur = $loc;
|
|
|
|
|
|
|
|
|
|
|
|
// 20 is the overflow limit in src/Entity/Base/AbstractStructuralDBElement.php line ~273
|
|
|
|
|
|
for ($i = 0; $i < 20 && $cur !== null; $i++) {
|
|
|
|
|
|
$items[] = [
|
|
|
|
|
|
'name' => $cur->getName(),
|
|
|
|
|
|
'url' => $this->generateUrl('part_list_store_location', ['id' => $cur->getID()]),
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
$parent = $cur->getParent(); // inherited from AbstractStructuralDBElement
|
|
|
|
|
|
$cur = ($parent instanceof StorageLocation) ? $parent : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return array_reverse($items);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[Route(path: '/augmented', name: 'scan_augmented', methods: ['POST'])]
|
|
|
|
|
|
public function augmented(Request $request): Response
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->denyAccessUnlessGranted('@tools.label_scanner');
|
|
|
|
|
|
|
|
|
|
|
|
$input = (string) $request->request->get('input', '');
|
|
|
|
|
|
$mode = $request->request->get('mode'); // string|null
|
|
|
|
|
|
|
|
|
|
|
|
if ($input === '') {
|
|
|
|
|
|
// Return empty fragment or an error fragment; your choice
|
|
|
|
|
|
return new Response('', 200);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$modeEnum = null;
|
|
|
|
|
|
if ($mode !== null && $mode !== '') {
|
|
|
|
|
|
// Radio values are enum integers in your form
|
|
|
|
|
|
$modeEnum = BarcodeSourceType::from((int) $mode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$scan = $this->barcodeNormalizer->scanBarcodeContent($input, $modeEnum);
|
|
|
|
|
|
$decoded = $scan->getDecodedForInfoMode();
|
|
|
|
|
|
|
|
|
|
|
|
$locale = $request->getLocale();
|
|
|
|
|
|
$part = $this->barcodeParser->resolvePartOrNull($scan);
|
|
|
|
|
|
|
|
|
|
|
|
$found = $part !== null;
|
|
|
|
|
|
$partName = null;
|
|
|
|
|
|
$partUrl = null;
|
|
|
|
|
|
$locations = [];
|
|
|
|
|
|
$createUrl = null;
|
|
|
|
|
|
|
|
|
|
|
|
if ($found) {
|
|
|
|
|
|
$partName = $part->getName();
|
|
|
|
|
|
|
|
|
|
|
|
// This is the same route BarcodeRedirector uses
|
|
|
|
|
|
$partUrl = $this->generateUrl('app_part_show', ['id' => $part->getID()]);
|
|
|
|
|
|
|
|
|
|
|
|
// Build locations (see below)
|
|
|
|
|
|
$locations = $this->buildLocationsForPart($part);
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Reuse your centralized create-url logic (the helper you already extracted)
|
|
|
|
|
|
$createUrl = $this->buildCreateUrlForScanResult($scan, $locale);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $this->render('label_system/scanner/augmented_result.html.twig', [
|
|
|
|
|
|
'decoded' => $decoded,
|
|
|
|
|
|
'found' => $found,
|
|
|
|
|
|
'partName' => $partName,
|
|
|
|
|
|
'partUrl' => $partUrl,
|
|
|
|
|
|
'locations' => $locations,
|
|
|
|
|
|
'createUrl' => $createUrl,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
2020-05-10 21:39:31 +02:00
|
|
|
|
}
|