mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-02-25 10:59:35 +00:00
Merge branch 'master' into amazon_info_provider
This commit is contained in:
commit
c6cbc17c66
34 changed files with 1625 additions and 687 deletions
|
|
@ -41,11 +41,16 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Exceptions\InfoProviderNotActiveException;
|
||||
use App\Form\LabelSystem\ScanDialogType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeRedirector;
|
||||
use App\Services\InfoProviderSystem\Providers\LCSCProvider;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanResultHandler;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanHelper;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeScanResultInterface;
|
||||
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
|
||||
use App\Services\LabelSystem\BarcodeScanner\LocalBarcodeScanResult;
|
||||
use App\Services\LabelSystem\BarcodeScanner\LCSCBarcodeScanResult;
|
||||
use App\Services\LabelSystem\BarcodeScanner\EIGP114BarcodeScanResult;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
|
@ -53,6 +58,13 @@ use Symfony\Component\HttpFoundation\Request;
|
|||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
||||
use App\Services\InfoProviderSystem\ProviderRegistry;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use App\Entity\Parts\Part;
|
||||
use \App\Entity\Parts\StorageLocation;
|
||||
use Symfony\UX\Turbo\TurboBundle;
|
||||
|
||||
/**
|
||||
* @see \App\Tests\Controller\ScanControllerTest
|
||||
|
|
@ -60,9 +72,10 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||
#[Route(path: '/scan')]
|
||||
class ScanController extends AbstractController
|
||||
{
|
||||
public function __construct(protected BarcodeRedirector $barcodeParser, protected BarcodeScanHelper $barcodeNormalizer)
|
||||
{
|
||||
}
|
||||
public function __construct(
|
||||
protected BarcodeScanResultHandler $resultHandler,
|
||||
protected BarcodeScanHelper $barcodeNormalizer,
|
||||
) {}
|
||||
|
||||
#[Route(path: '', name: 'scan_dialog')]
|
||||
public function dialog(Request $request, #[MapQueryParameter] ?string $input = null): Response
|
||||
|
|
@ -72,35 +85,86 @@ class ScanController extends AbstractController
|
|||
$form = $this->createForm(ScanDialogType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
// If JS is working, scanning uses /scan/lookup and this action just renders the page.
|
||||
// This fallback only runs if user submits the form manually or uses ?input=...
|
||||
if ($input === null && $form->isSubmitted() && $form->isValid()) {
|
||||
$input = $form['input']->getData();
|
||||
$mode = $form['mode']->getData();
|
||||
}
|
||||
|
||||
$infoModeData = null;
|
||||
|
||||
if ($input !== null) {
|
||||
if ($input !== null && $input !== '') {
|
||||
$mode = $form->isSubmitted() ? $form['mode']->getData() : null;
|
||||
$infoMode = $form->isSubmitted() && $form['info_mode']->getData();
|
||||
|
||||
try {
|
||||
$scan_result = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
|
||||
//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');
|
||||
$scan = $this->barcodeNormalizer->scanBarcodeContent($input, $mode ?? null);
|
||||
|
||||
// If not in info mode, mimic “normal scan” behavior: redirect if possible.
|
||||
if (!$infoMode) {
|
||||
|
||||
// Try to get an Info URL if possible
|
||||
$url = $this->resultHandler->getInfoURL($scan);
|
||||
if ($url !== null) {
|
||||
return $this->redirect($url);
|
||||
}
|
||||
|
||||
//Try to get an creation URL if possible (only for vendor codes)
|
||||
$createUrl = $this->buildCreateUrlForScanResult($scan);
|
||||
if ($createUrl !== null) {
|
||||
return $this->redirect($createUrl);
|
||||
}
|
||||
|
||||
//// Otherwise: show “not found” (not “format unknown”)
|
||||
$this->addFlash('warning', 'scan.qr_not_found');
|
||||
} else { // Info mode
|
||||
// Info mode fallback: render page with prefilled result
|
||||
$decoded = $scan->getDecodedForInfoMode();
|
||||
|
||||
//Try to resolve to an entity, to enhance info mode with entity-specific data
|
||||
$dbEntity = $this->resultHandler->resolveEntity($scan);
|
||||
$resolvedPart = $this->resultHandler->resolvePart($scan);
|
||||
$openUrl = $this->resultHandler->getInfoURL($scan);
|
||||
|
||||
//If no entity is found, try to create an URL for creating a new part (only for vendor codes)
|
||||
$createUrl = null;
|
||||
if ($dbEntity === null) {
|
||||
$createUrl = $this->buildCreateUrlForScanResult($scan);
|
||||
}
|
||||
|
||||
if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
|
||||
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
|
||||
return $this->renderBlock('label_system/scanner/scanner.html.twig', 'scan_results', [
|
||||
'decoded' => $decoded,
|
||||
'entity' => $dbEntity,
|
||||
'part' => $resolvedPart,
|
||||
'openUrl' => $openUrl,
|
||||
'createUrl' => $createUrl,
|
||||
]);
|
||||
}
|
||||
} else { //Otherwise retrieve infoModeData
|
||||
$infoModeData = $scan_result->getDecodedForInfoMode();
|
||||
|
||||
}
|
||||
} catch (InvalidArgumentException) {
|
||||
$this->addFlash('error', 'scan.format_unknown');
|
||||
} catch (\Throwable $e) {
|
||||
// Keep fallback user-friendly; avoid 500
|
||||
$this->addFlash('warning', 'scan.format_unknown');
|
||||
}
|
||||
}
|
||||
|
||||
//When we reach here, only the flash messages are relevant, so if it's a Turbo request, only send the flash message fragment, so the client can show it without a full page reload
|
||||
if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
|
||||
$request->setRequestFormat(TurboBundle::STREAM_FORMAT);
|
||||
//Only send our flash message, so the client can show it without a full page reload
|
||||
return $this->renderBlock('_turbo_control.html.twig', 'flashes');
|
||||
}
|
||||
|
||||
return $this->render('label_system/scanner/scanner.html.twig', [
|
||||
'form' => $form,
|
||||
'infoModeData' => $infoModeData,
|
||||
|
||||
//Info mode
|
||||
'decoded' => $decoded ?? null,
|
||||
'entity' => $dbEntity ?? null,
|
||||
'part' => $resolvedPart ?? null,
|
||||
'openUrl' => $openUrl ?? null,
|
||||
'createUrl' => $createUrl ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -125,11 +189,30 @@ class ScanController extends AbstractController
|
|||
source_type: BarcodeSourceType::INTERNAL
|
||||
);
|
||||
|
||||
return $this->redirect($this->barcodeParser->getRedirectURL($scan_result));
|
||||
return $this->redirect($this->resultHandler->getInfoURL($scan_result) ?? throw new EntityNotFoundException("Not found"));
|
||||
} catch (EntityNotFoundException) {
|
||||
$this->addFlash('success', 'scan.qr_not_found');
|
||||
|
||||
return $this->redirectToRoute('homepage');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a URL for creating a new part based on the barcode data, handles exceptions and shows user-friendly error messages if the provider is not active or if there is an error during URL generation.
|
||||
* @param BarcodeScanResultInterface $scanResult
|
||||
* @return string|null
|
||||
*/
|
||||
private function buildCreateUrlForScanResult(BarcodeScanResultInterface $scanResult): ?string
|
||||
{
|
||||
try {
|
||||
return $this->resultHandler->getCreationURL($scanResult);
|
||||
} catch (InfoProviderNotActiveException $e) {
|
||||
$this->addFlash('error', $e->getMessage());
|
||||
} catch (\Throwable) {
|
||||
// Don’t break scanning UX if provider lookup fails
|
||||
$this->addFlash('error', 'An error occurred while looking up the provider for this barcode. Please try again later.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
src/Exceptions/InfoProviderNotActiveException.php
Normal file
48
src/Exceptions/InfoProviderNotActiveException.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 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\Exceptions;
|
||||
|
||||
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
|
||||
|
||||
/**
|
||||
* An exception denoting that a required info provider is not active. This can be used to display a user-friendly error message,
|
||||
* when a user tries to use an info provider that is not active.
|
||||
*/
|
||||
class InfoProviderNotActiveException extends \RuntimeException
|
||||
{
|
||||
public function __construct(public readonly string $providerKey, public readonly string $friendlyName)
|
||||
{
|
||||
parent::__construct(sprintf('The info provider "%s" (%s) is not active.', $this->friendlyName, $this->providerKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of this exception from an info provider instance
|
||||
* @param InfoProviderInterface $provider
|
||||
* @return self
|
||||
*/
|
||||
public static function fromProvider(InfoProviderInterface $provider): self
|
||||
{
|
||||
return new self($provider->getProviderKey(), $provider->getProviderInfo()['name'] ?? '???');
|
||||
}
|
||||
}
|
||||
|
|
@ -77,6 +77,7 @@ class ScanDialogType extends AbstractType
|
|||
BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user',
|
||||
BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp',
|
||||
BarcodeSourceType::GTIN => 'scan_dialog.mode.gtin',
|
||||
BarcodeSourceType::LCSC => 'scan_dialog.mode.lcsc',
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -389,4 +389,69 @@ class PartRepository extends NamedDBElementRepository
|
|||
return $baseIpn . '_' . ($maxSuffix + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a part based on the provided info provider key and ID, with an option for case sensitivity.
|
||||
* If no part is found with the given provider key and ID, null is returned.
|
||||
* @param string $providerID
|
||||
* @param string|null $providerKey If null, the provider key will not be included in the search criteria, and only the provider ID will be used for matching.
|
||||
* @param bool $caseInsensitive If true, the provider ID comparison will be case-insensitive. Default is true.
|
||||
* @return Part|null
|
||||
*/
|
||||
public function getPartByProviderInfo(string $providerID, ?string $providerKey = null, bool $caseInsensitive = true): ?Part
|
||||
{
|
||||
$qb = $this->createQueryBuilder('part');
|
||||
$qb->select('part');
|
||||
|
||||
if ($providerKey) {
|
||||
$qb->where("part.providerReference.provider_key = :providerKey");
|
||||
$qb->setParameter('providerKey', $providerKey);
|
||||
}
|
||||
|
||||
|
||||
if ($caseInsensitive) {
|
||||
$qb->andWhere("LOWER(part.providerReference.provider_id) = LOWER(:providerID)");
|
||||
} else {
|
||||
$qb->andWhere("part.providerReference.provider_id = :providerID");
|
||||
}
|
||||
|
||||
$qb->setParameter('providerID', $providerID);
|
||||
|
||||
return $qb->getQuery()->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a part based on the provided MPN (Manufacturer Part Number), with an option for case sensitivity.
|
||||
* If no part is found with the given MPN, null is returned.
|
||||
* @param string $mpn
|
||||
* @param string|null $manufacturerName If provided, the search will also include a match for the manufacturer's name. If null, the manufacturer name will not be included in the search criteria.
|
||||
* @param bool $caseInsensitive If true, the MPN comparison will be case-insensitive. Default is true (case-insensitive).
|
||||
* @return Part|null
|
||||
*/
|
||||
public function getPartByMPN(string $mpn, ?string $manufacturerName = null, bool $caseInsensitive = true): ?Part
|
||||
{
|
||||
$qb = $this->createQueryBuilder('part');
|
||||
$qb->select('part');
|
||||
|
||||
if ($caseInsensitive) {
|
||||
$qb->where("LOWER(part.manufacturer_product_number) = LOWER(:mpn)");
|
||||
} else {
|
||||
$qb->where("part.manufacturer_product_number = :mpn");
|
||||
}
|
||||
|
||||
if ($manufacturerName !== null) {
|
||||
$qb->leftJoin('part.manufacturer', 'manufacturer');
|
||||
|
||||
if ($caseInsensitive) {
|
||||
$qb->andWhere("LOWER(manufacturer.name) = LOWER(:manufacturerName)");
|
||||
} else {
|
||||
$qb->andWhere("manufacturer.name = :manufacturerName");
|
||||
}
|
||||
$qb->setParameter('manufacturerName', $manufacturerName);
|
||||
}
|
||||
|
||||
$qb->setParameter('mpn', $mpn);
|
||||
|
||||
return $qb->getQuery()->getOneOrNullResult();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,15 @@ declare(strict_types=1);
|
|||
namespace App\Services\InfoProviderSystem;
|
||||
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Exceptions\InfoProviderNotActiveException;
|
||||
use App\Exceptions\OAuthReconnectRequiredException;
|
||||
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
|
|
@ -49,6 +54,11 @@ final class PartInfoRetriever
|
|||
* @param string[]|InfoProviderInterface[] $providers A list of providers to search in, either as provider keys or as provider instances
|
||||
* @param string $keyword The keyword to search for
|
||||
* @return SearchResultDTO[] The search results
|
||||
* @throws InfoProviderNotActiveException if any of the given providers is not active
|
||||
* @throws ClientException if any of the providers throws an exception during the search
|
||||
* @throws \InvalidArgumentException if any of the given providers is not a valid provider key or instance
|
||||
* @throws TransportException if any of the providers throws an exception during the search
|
||||
* @throws OAuthReconnectRequiredException if any of the providers throws an exception during the search that indicates that the OAuth token needs to be refreshed
|
||||
*/
|
||||
public function searchByKeyword(string $keyword, array $providers): array
|
||||
{
|
||||
|
|
@ -61,7 +71,7 @@ final class PartInfoRetriever
|
|||
|
||||
//Ensure that the provider is active
|
||||
if (!$provider->isActive()) {
|
||||
throw new \RuntimeException("The provider with key {$provider->getProviderKey()} is not active!");
|
||||
throw InfoProviderNotActiveException::fromProvider($provider);
|
||||
}
|
||||
|
||||
if (!$provider instanceof InfoProviderInterface) {
|
||||
|
|
@ -97,6 +107,7 @@ final class PartInfoRetriever
|
|||
* @param string $provider_key
|
||||
* @param string $part_id
|
||||
* @return PartDetailDTO
|
||||
* @throws InfoProviderNotActiveException if the the given providers is not active
|
||||
*/
|
||||
public function getDetails(string $provider_key, string $part_id): PartDetailDTO
|
||||
{
|
||||
|
|
@ -104,7 +115,7 @@ final class PartInfoRetriever
|
|||
|
||||
//Ensure that the provider is active
|
||||
if (!$provider->isActive()) {
|
||||
throw new \RuntimeException("The provider with key $provider_key is not active!");
|
||||
throw InfoProviderNotActiveException::fromProvider($provider);
|
||||
}
|
||||
|
||||
//Generate key and escape reserved characters from the provider id
|
||||
|
|
|
|||
|
|
@ -1,180 +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\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);
|
||||
}
|
||||
|
||||
if ($barcodeScan instanceof GTINBarcodeScanResult) {
|
||||
return $this->getURLGTINBarcode($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(), 'highlightLot' => $lot->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()]);
|
||||
}
|
||||
|
||||
private function getURLGTINBarcode(GTINBarcodeScanResult $barcodeScan): string
|
||||
{
|
||||
$part = $this->em->getRepository(Part::class)->findOneBy(['gtin' => $barcodeScan->gtin]);
|
||||
if (!$part instanceof Part) {
|
||||
throw new EntityNotFoundException();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -92,10 +92,15 @@ final class BarcodeScanHelper
|
|||
if ($type === BarcodeSourceType::EIGP114) {
|
||||
return $this->parseEIGP114Barcode($input);
|
||||
}
|
||||
|
||||
if ($type === BarcodeSourceType::GTIN) {
|
||||
return $this->parseGTINBarcode($input);
|
||||
}
|
||||
|
||||
if ($type === BarcodeSourceType::LCSC) {
|
||||
return $this->parseLCSCBarcode($input);
|
||||
}
|
||||
|
||||
//Null means auto and we try the different formats
|
||||
$result = $this->parseInternalBarcode($input);
|
||||
|
||||
|
|
@ -125,6 +130,11 @@ final class BarcodeScanHelper
|
|||
return $this->parseGTINBarcode($input);
|
||||
}
|
||||
|
||||
// Try LCSC barcode
|
||||
if (LCSCBarcodeScanResult::isLCSCBarcode($input)) {
|
||||
return $this->parseLCSCBarcode($input);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unknown barcode');
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +148,11 @@ final class BarcodeScanHelper
|
|||
return EIGP114BarcodeScanResult::parseFormat06Code($input);
|
||||
}
|
||||
|
||||
private function parseLCSCBarcode(string $input): LCSCBarcodeScanResult
|
||||
{
|
||||
return LCSCBarcodeScanResult::parse($input);
|
||||
}
|
||||
|
||||
private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult
|
||||
{
|
||||
$lot_repo = $this->entityManager->getRepository(PartLot::class);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,315 @@
|
|||
<?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 App\Entity\Parts\StorageLocation;
|
||||
use App\Exceptions\InfoProviderNotActiveException;
|
||||
use App\Repository\Parts\PartRepository;
|
||||
use App\Services\InfoProviderSystem\PartInfoRetriever;
|
||||
use App\Services\InfoProviderSystem\ProviderRegistry;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* This class handles the result of a barcode scan and determines further actions, like which URL the user should be redirected to.
|
||||
*
|
||||
* @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeRedirectorTest
|
||||
*/
|
||||
final readonly class BarcodeScanResultHandler
|
||||
{
|
||||
public function __construct(private UrlGeneratorInterface $urlGenerator, private EntityManagerInterface $em, private PartInfoRetriever $infoRetriever,
|
||||
private ProviderRegistry $providerRegistry)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 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|null the URL to which should be redirected, or null if no suitable URL could be determined for the given barcode scan result
|
||||
*/
|
||||
public function getInfoURL(BarcodeScanResultInterface $barcodeScan): ?string
|
||||
{
|
||||
//For other barcodes try to resolve the part first and then redirect to the part page
|
||||
$entity = $this->resolveEntity($barcodeScan);
|
||||
|
||||
if ($entity === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($entity instanceof Part) {
|
||||
return $this->urlGenerator->generate('app_part_show', ['id' => $entity->getID()]);
|
||||
}
|
||||
|
||||
if ($entity instanceof PartLot) {
|
||||
return $this->urlGenerator->generate('app_part_show', ['id' => $entity->getPart()->getID(), 'highlightLot' => $entity->getID()]);
|
||||
}
|
||||
|
||||
if ($entity instanceof StorageLocation) {
|
||||
return $this->urlGenerator->generate('part_list_store_location', ['id' => $entity->getID()]);
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line This should never happen, since resolveEntity should only return Part, PartLot or StorageLocation
|
||||
throw new \LogicException("Resolved entity is of unknown type: ".get_class($entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a URL to create a new part based on this barcode scan result, if possible.
|
||||
* @param BarcodeScanResultInterface $scanResult
|
||||
* @return string|null
|
||||
* @throws InfoProviderNotActiveException If the scan result contains information for a provider which is currently not active in the system
|
||||
*/
|
||||
public function getCreationURL(BarcodeScanResultInterface $scanResult): ?string
|
||||
{
|
||||
$infos = $this->getCreateInfos($scanResult);
|
||||
if ($infos === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//Ensure that the provider is active, otherwise we should not generate a creation URL for it
|
||||
$provider = $this->providerRegistry->getProviderByKey($infos['providerKey']);
|
||||
if (!$provider->isActive()) {
|
||||
throw InfoProviderNotActiveException::fromProvider($provider);
|
||||
}
|
||||
|
||||
return $this->urlGenerator->generate('info_providers_create_part', ['providerKey' => $infos['providerKey'], 'providerId' => $infos['providerId']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to resolve the given barcode scan result to a local entity. This can be a Part, a PartLot or a StorageLocation, depending on the type of the barcode and the information contained in it.
|
||||
* Returns null if no matching entity could be found.
|
||||
* @param BarcodeScanResultInterface $barcodeScan
|
||||
* @return Part|PartLot|StorageLocation|null
|
||||
*/
|
||||
public function resolveEntity(BarcodeScanResultInterface $barcodeScan): Part|PartLot|StorageLocation|null
|
||||
{
|
||||
if ($barcodeScan instanceof LocalBarcodeScanResult) {
|
||||
return $this->resolvePartFromLocal($barcodeScan);
|
||||
}
|
||||
|
||||
if ($barcodeScan instanceof EIGP114BarcodeScanResult) {
|
||||
return $this->resolvePartFromVendor($barcodeScan);
|
||||
}
|
||||
|
||||
if ($barcodeScan instanceof GTINBarcodeScanResult) {
|
||||
return $this->em->getRepository(Part::class)->findOneBy(['gtin' => $barcodeScan->gtin]);
|
||||
}
|
||||
|
||||
if ($barcodeScan instanceof LCSCBarcodeScanResult) {
|
||||
return $this->resolvePartFromLCSC($barcodeScan);
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException("Barcode does not support resolving to a local entity: ".get_class($barcodeScan));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to resolve a Part from the given barcode scan result. Returns null if no part could be found for the given barcode,
|
||||
* or the barcode doesn't contain information allowing to resolve to a local part.
|
||||
* @param BarcodeScanResultInterface $barcodeScan
|
||||
* @return Part|null
|
||||
* @throws \InvalidArgumentException if the barcode scan result type is unknown and cannot be handled this function
|
||||
*/
|
||||
public function resolvePart(BarcodeScanResultInterface $barcodeScan): ?Part
|
||||
{
|
||||
$entity = $this->resolveEntity($barcodeScan);
|
||||
if ($entity instanceof Part) {
|
||||
return $entity;
|
||||
}
|
||||
if ($entity instanceof PartLot) {
|
||||
return $entity->getPart();
|
||||
}
|
||||
//Storage locations are not associated with a specific part, so we cannot resolve a part for
|
||||
//a storage location barcode
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolvePartFromLocal(LocalBarcodeScanResult $barcodeScan): Part|PartLot|StorageLocation|null
|
||||
{
|
||||
return match ($barcodeScan->target_type) {
|
||||
LabelSupportedElement::PART => $this->em->find(Part::class, $barcodeScan->target_id),
|
||||
LabelSupportedElement::PART_LOT => $this->em->find(PartLot::class, $barcodeScan->target_id),
|
||||
LabelSupportedElement::STORELOCATION => $this->em->find(StorageLocation::class, $barcodeScan->target_id),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 resolvePartFromVendor(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) {
|
||||
|
||||
$part = $this->em->getRepository(Part::class)->getPartByProviderInfo($barcodeScan->digikeyPartNumber);
|
||||
if ($part !== null) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$barcodeScan->supplierPartNumber){
|
||||
return null;
|
||||
}
|
||||
|
||||
//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
|
||||
|
||||
return $this->em->getRepository(Part::class)->getPartByMPN($barcodeScan->supplierPartNumber, $barcodeScan->mouserManufacturer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve LCSC barcode -> Part.
|
||||
* Strategy:
|
||||
* 1) Try providerReference.provider_id == pc (LCSC "Cxxxxxx") if you store it there
|
||||
* 2) Fallback to manufacturer_product_number == pm (MPN)
|
||||
* Returns first match (consistent with EIGP114 logic)
|
||||
*/
|
||||
private function resolvePartFromLCSC(LCSCBarcodeScanResult $barcodeScan): ?Part
|
||||
{
|
||||
// Try LCSC code (pc) as provider id if available
|
||||
$pc = $barcodeScan->lcscCode; // e.g. C138033
|
||||
if ($pc) {
|
||||
$part = $this->em->getRepository(Part::class)->getPartByProviderInfo($pc);
|
||||
if ($part !== null) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to MPN (pm)
|
||||
$pm = $barcodeScan->mpn; // e.g. RC0402FR-071ML
|
||||
if (!$pm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->em->getRepository(Part::class)->getPartByMPN($pm);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tries to extract creation information for a part from the given barcode scan result. This can be used to
|
||||
* automatically fill in the info provider reference of a part, when creating a new part based on the scan result.
|
||||
* Returns null if no provider information could be extracted from the scan result, or if the scan result type is unknown and cannot be handled by this function.
|
||||
* It is not necessarily checked that the provider is active, or that the result actually exists on the provider side.
|
||||
* @param BarcodeScanResultInterface $scanResult
|
||||
* @return array{providerKey: string, providerId: string}|null
|
||||
* @throws InfoProviderNotActiveException If the scan result contains information for a provider which is currently not active in the system
|
||||
*/
|
||||
public function getCreateInfos(BarcodeScanResultInterface $scanResult): ?array
|
||||
{
|
||||
// LCSC
|
||||
if ($scanResult instanceof LCSCBarcodeScanResult) {
|
||||
return [
|
||||
'providerKey' => 'lcsc',
|
||||
'providerId' => $scanResult->lcscCode,
|
||||
];
|
||||
}
|
||||
|
||||
if ($scanResult instanceof EIGP114BarcodeScanResult) {
|
||||
return $this->getCreationInfoForEIGP114($scanResult);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EIGP114BarcodeScanResult $scanResult
|
||||
* @return array{providerKey: string, providerId: string}|null
|
||||
*/
|
||||
private function getCreationInfoForEIGP114(EIGP114BarcodeScanResult $scanResult): ?array
|
||||
{
|
||||
$vendor = $scanResult->guessBarcodeVendor();
|
||||
|
||||
// Mouser: use supplierPartNumber -> search provider -> provider_id
|
||||
if ($vendor === 'mouser' && $scanResult->supplierPartNumber !== null
|
||||
) {
|
||||
// Search Mouser using the MPN
|
||||
$dtos = $this->infoRetriever->searchByKeyword(
|
||||
keyword: $scanResult->supplierPartNumber,
|
||||
providers: ["mouser"]
|
||||
);
|
||||
|
||||
// If there are results, provider_id is MouserPartNumber (per MouserProvider.php)
|
||||
$best = $dtos[0] ?? null;
|
||||
|
||||
if ($best !== null) {
|
||||
return [
|
||||
'providerKey' => 'mouser',
|
||||
'providerId' => $best->provider_id,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Digi-Key: can use customerPartNumber or supplierPartNumber directly
|
||||
if ($vendor === 'digikey') {
|
||||
return [
|
||||
'providerKey' => 'digikey',
|
||||
'providerId' => $scanResult->customerPartNumber ?? $scanResult->supplierPartNumber,
|
||||
];
|
||||
}
|
||||
|
||||
// Element14: can use supplierPartNumber directly
|
||||
if ($vendor === 'element14') {
|
||||
return [
|
||||
'providerKey' => 'element14',
|
||||
'providerId' => $scanResult->supplierPartNumber,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -33,4 +33,4 @@ interface BarcodeScanResultInterface
|
|||
* @return array<string, string|int|float|null>
|
||||
*/
|
||||
public function getDecodedForInfoMode(): array;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,25 +26,28 @@ namespace App\Services\LabelSystem\BarcodeScanner;
|
|||
/**
|
||||
* This enum represents the different types, where a barcode/QR-code can be generated from
|
||||
*/
|
||||
enum BarcodeSourceType
|
||||
enum BarcodeSourceType: string
|
||||
{
|
||||
/** This Barcode was generated using Part-DB internal recommended barcode generator */
|
||||
case INTERNAL;
|
||||
case INTERNAL = 'internal';
|
||||
/** This barcode is containing an internal part number (IPN) */
|
||||
case IPN;
|
||||
case IPN = 'ipn';
|
||||
|
||||
/**
|
||||
* This barcode is a user defined barcode defined on a part lot
|
||||
*/
|
||||
case USER_DEFINED;
|
||||
case USER_DEFINED = 'user_defined';
|
||||
|
||||
/**
|
||||
* EIGP114 formatted barcodes like used by digikey, mouser, etc.
|
||||
*/
|
||||
case EIGP114;
|
||||
case EIGP114 = 'eigp114';
|
||||
|
||||
/**
|
||||
* GTIN /EAN barcodes, which are used on most products in the world. These are checked with the GTIN field of a part.
|
||||
*/
|
||||
case GTIN;
|
||||
case GTIN = 'gtin';
|
||||
|
||||
/** For LCSC.com formatted QR codes */
|
||||
case LCSC = 'lcsc';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,40 +28,40 @@ namespace App\Services\LabelSystem\BarcodeScanner;
|
|||
* 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
|
||||
readonly class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string|null Ship date in format YYYYMMDD
|
||||
*/
|
||||
public readonly ?string $shipDate;
|
||||
public ?string $shipDate;
|
||||
|
||||
/**
|
||||
* @var string|null Customer assigned part number – Optional based on
|
||||
* agreements between Distributor and Supplier
|
||||
*/
|
||||
public readonly ?string $customerPartNumber;
|
||||
public ?string $customerPartNumber;
|
||||
|
||||
/**
|
||||
* @var string|null Supplier assigned part number
|
||||
*/
|
||||
public readonly ?string $supplierPartNumber;
|
||||
public ?string $supplierPartNumber;
|
||||
|
||||
/**
|
||||
* @var int|null Quantity of product
|
||||
*/
|
||||
public readonly ?int $quantity;
|
||||
public ?int $quantity;
|
||||
|
||||
/**
|
||||
* @var string|null Customer assigned purchase order number
|
||||
*/
|
||||
public readonly ?string $customerPO;
|
||||
public ?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;
|
||||
public ?string $customerPOLine;
|
||||
|
||||
/**
|
||||
* 9D - YYWW (Year and Week of Manufacture). ) If no date code is used
|
||||
|
|
@ -69,7 +69,7 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
* to indicate the product is Not Traceable by this data field.
|
||||
* @var string|null
|
||||
*/
|
||||
public readonly ?string $dateCode;
|
||||
public ?string $dateCode;
|
||||
|
||||
/**
|
||||
* 10D - YYWW (Year and Week of Manufacture). ) If no date code is used
|
||||
|
|
@ -77,7 +77,7 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
* to indicate the product is Not Traceable by this data field.
|
||||
* @var string|null
|
||||
*/
|
||||
public readonly ?string $alternativeDateCode;
|
||||
public ?string $alternativeDateCode;
|
||||
|
||||
/**
|
||||
* Traceability number assigned to a batch or group of items. If
|
||||
|
|
@ -86,14 +86,14 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
* by this data field.
|
||||
* @var string|null
|
||||
*/
|
||||
public readonly ?string $lotCode;
|
||||
public ?string $lotCode;
|
||||
|
||||
/**
|
||||
* Country where part was manufactured. Two-letter code from
|
||||
* ISO 3166 country code list
|
||||
* @var string|null
|
||||
*/
|
||||
public readonly ?string $countryOfOrigin;
|
||||
public ?string $countryOfOrigin;
|
||||
|
||||
/**
|
||||
* @var string|null Unique alphanumeric number assigned by supplier
|
||||
|
|
@ -101,85 +101,85 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
* Carton. Always used in conjunction with a mixed logistic label
|
||||
* with a 5S data identifier for Package ID.
|
||||
*/
|
||||
public readonly ?string $packageId1;
|
||||
public ?string $packageId1;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
* 4S - Package ID for Logistic Carton with like items
|
||||
*/
|
||||
public readonly ?string $packageId2;
|
||||
public ?string $packageId2;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
* 5S - Package ID for Logistic Carton with mixed items
|
||||
*/
|
||||
public readonly ?string $packageId3;
|
||||
public ?string $packageId3;
|
||||
|
||||
/**
|
||||
* @var string|null Unique alphanumeric number assigned by supplier.
|
||||
*/
|
||||
public readonly ?string $packingListNumber;
|
||||
public ?string $packingListNumber;
|
||||
|
||||
/**
|
||||
* @var string|null Ship date in format YYYYMMDD
|
||||
*/
|
||||
public readonly ?string $serialNumber;
|
||||
public ?string $serialNumber;
|
||||
|
||||
/**
|
||||
* @var string|null Code for sorting and classifying LEDs. Use when applicable
|
||||
*/
|
||||
public readonly ?string $binCode;
|
||||
public ?string $binCode;
|
||||
|
||||
/**
|
||||
* @var int|null Sequential carton count in format “#/#” or “# of #”
|
||||
*/
|
||||
public readonly ?int $packageCount;
|
||||
public ?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;
|
||||
public ?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;
|
||||
public ?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;
|
||||
public ?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;
|
||||
public ?string $digikeyInvoiceNumber;
|
||||
|
||||
/**
|
||||
* @var string|null Digikey extension: This is for internal DigiKey purposes and defines the label type.
|
||||
*/
|
||||
public readonly ?string $digikeyLabelType;
|
||||
public ?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;
|
||||
public ?string $digikeyPartID;
|
||||
|
||||
/**
|
||||
* @var string|null Digikey Extension: For internal use of Digikey. Probably not needed
|
||||
*/
|
||||
public readonly ?string $digikeyNA;
|
||||
public ?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 ?string $digikeyPadding;
|
||||
|
||||
public readonly ?string $mouserPositionInOrder;
|
||||
public ?string $mouserPositionInOrder;
|
||||
|
||||
public readonly ?string $mouserManufacturer;
|
||||
public ?string $mouserManufacturer;
|
||||
|
||||
|
||||
|
||||
|
|
@ -187,7 +187,7 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
*
|
||||
* @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)
|
||||
public function __construct(public array $data)
|
||||
{
|
||||
//IDs per EIGP 114.2018
|
||||
$this->shipDate = $data['6D'] ?? null;
|
||||
|
|
@ -329,4 +329,4 @@ class EIGP114BarcodeScanResult implements BarcodeScanResultInterface
|
|||
|
||||
return $tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\LabelSystem\BarcodeScanner;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* This class represents the content of a lcsc.com barcode
|
||||
* Its data structure is represented by {pbn:...,on:...,pc:...,pm:...,qty:...}
|
||||
*/
|
||||
readonly class LCSCBarcodeScanResult implements BarcodeScanResultInterface
|
||||
{
|
||||
|
||||
/** @var string|null (pbn) */
|
||||
public ?string $pickBatchNumber;
|
||||
|
||||
/** @var string|null (on) */
|
||||
public ?string $orderNumber;
|
||||
|
||||
/** @var string|null LCSC Supplier part number (pc) */
|
||||
public ?string $lcscCode;
|
||||
|
||||
/** @var string|null (pm) */
|
||||
public ?string $mpn;
|
||||
|
||||
/** @var int|null (qty) */
|
||||
public ?int $quantity;
|
||||
|
||||
/** @var string|null Country Channel as raw value (CC) */
|
||||
public ?string $countryChannel;
|
||||
|
||||
/**
|
||||
* @var string|null Warehouse code as raw value (WC)
|
||||
*/
|
||||
public ?string $warehouseCode;
|
||||
|
||||
/**
|
||||
* @var string|null Unknown numeric code (pdi)
|
||||
*/
|
||||
public ?string $pdi;
|
||||
|
||||
/**
|
||||
* @var string|null Unknown value (hp)
|
||||
*/
|
||||
public ?string $hp;
|
||||
|
||||
/**
|
||||
* @param array<string, string> $fields
|
||||
*/
|
||||
public function __construct(
|
||||
public array $fields,
|
||||
public string $rawInput,
|
||||
) {
|
||||
|
||||
$this->pickBatchNumber = $this->fields['pbn'] ?? null;
|
||||
$this->orderNumber = $this->fields['on'] ?? null;
|
||||
$this->lcscCode = $this->fields['pc'] ?? null;
|
||||
$this->mpn = $this->fields['pm'] ?? null;
|
||||
$this->quantity = isset($this->fields['qty']) ? (int)$this->fields['qty'] : null;
|
||||
$this->countryChannel = $this->fields['cc'] ?? null;
|
||||
$this->warehouseCode = $this->fields['wc'] ?? null;
|
||||
$this->pdi = $this->fields['pdi'] ?? null;
|
||||
$this->hp = $this->fields['hp'] ?? null;
|
||||
|
||||
}
|
||||
|
||||
public function getSourceType(): BarcodeSourceType
|
||||
{
|
||||
return BarcodeSourceType::LCSC;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|float[]|int[]|null[]|string[] An array of fields decoded from the barcode
|
||||
*/
|
||||
public function getDecodedForInfoMode(): array
|
||||
{
|
||||
// Keep it human-friendly
|
||||
return [
|
||||
'Barcode type' => 'LCSC',
|
||||
'MPN (pm)' => $this->mpn ?? '',
|
||||
'LCSC code (pc)' => $this->lcscCode ?? '',
|
||||
'Qty' => $this->quantity !== null ? (string) $this->quantity : '',
|
||||
'Order No (on)' => $this->orderNumber ?? '',
|
||||
'Pick Batch (pbn)' => $this->pickBatchNumber ?? '',
|
||||
'Warehouse (wc)' => $this->warehouseCode ?? '',
|
||||
'Country/Channel (cc)' => $this->countryChannel ?? '',
|
||||
'PDI (unknown meaning)' => $this->pdi ?? '',
|
||||
'HP (unknown meaning)' => $this->hp ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the barcode data to see if the input matches the expected format used by lcsc.com
|
||||
* @param string $input
|
||||
* @return bool
|
||||
*/
|
||||
public static function isLCSCBarcode(string $input): bool
|
||||
{
|
||||
$s = trim($input);
|
||||
|
||||
// Your example: {pbn:...,on:...,pc:...,pm:...,qty:...}
|
||||
if (!str_starts_with($s, '{') || !str_ends_with($s, '}')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must contain at least pm: and pc: (common for LCSC labels)
|
||||
return (stripos($s, 'pm:') !== false) && (stripos($s, 'pc:') !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the barcode input string into the fields used by lcsc.com
|
||||
* @param string $input
|
||||
* @return self
|
||||
*/
|
||||
public static function parse(string $input): self
|
||||
{
|
||||
$raw = trim($input);
|
||||
|
||||
if (!self::isLCSCBarcode($raw)) {
|
||||
throw new InvalidArgumentException('Not an LCSC barcode');
|
||||
}
|
||||
|
||||
$inner = substr($raw, 1, -1); // remove { }
|
||||
|
||||
$fields = [];
|
||||
|
||||
// This format is comma-separated pairs, values do not contain commas in your sample.
|
||||
$pairs = array_filter(
|
||||
array_map(trim(...), explode(',', $inner)),
|
||||
static fn(string $s): bool => $s !== ''
|
||||
);
|
||||
|
||||
foreach ($pairs as $pair) {
|
||||
$pos = strpos($pair, ':');
|
||||
if ($pos === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$k = trim(substr($pair, 0, $pos));
|
||||
$v = trim(substr($pair, $pos + 1));
|
||||
|
||||
if ($k === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fields[$k] = $v;
|
||||
}
|
||||
|
||||
if (!isset($fields['pm']) || trim($fields['pm']) === '') {
|
||||
throw new InvalidArgumentException('LCSC barcode missing pm field');
|
||||
}
|
||||
|
||||
return new self($fields, $raw);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,12 +29,12 @@ use App\Entity\LabelSystem\LabelSupportedElement;
|
|||
* 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 LocalBarcodeScanResult implements BarcodeScanResultInterface
|
||||
readonly class LocalBarcodeScanResult implements BarcodeScanResultInterface
|
||||
{
|
||||
public function __construct(
|
||||
public readonly LabelSupportedElement $target_type,
|
||||
public readonly int $target_id,
|
||||
public readonly BarcodeSourceType $source_type,
|
||||
public LabelSupportedElement $target_type,
|
||||
public int $target_id,
|
||||
public BarcodeSourceType $source_type,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -46,4 +46,4 @@ class LocalBarcodeScanResult implements BarcodeScanResultInterface
|
|||
'Target ID' => $this->target_id,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ declare(strict_types=1);
|
|||
namespace App\Twig;
|
||||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentContainingDBElement;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Services\Attachments\AttachmentURLGenerator;
|
||||
use App\Services\Attachments\PartPreviewGenerator;
|
||||
use App\Services\Misc\FAIconGenerator;
|
||||
use Twig\Attribute\AsTwigFunction;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
|
|
@ -31,7 +34,7 @@ use Twig\TwigFunction;
|
|||
|
||||
final readonly class AttachmentExtension
|
||||
{
|
||||
public function __construct(private AttachmentURLGenerator $attachmentURLGenerator, private FAIconGenerator $FAIconGenerator)
|
||||
public function __construct(private AttachmentURLGenerator $attachmentURLGenerator, private FAIconGenerator $FAIconGenerator, private PartPreviewGenerator $partPreviewGenerator)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +47,26 @@ final readonly class AttachmentExtension
|
|||
return $this->attachmentURLGenerator->getThumbnailURL($attachment, $filter_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the thumbnail of the given element. Returns null if no thumbnail is available.
|
||||
* For parts, a special preview image is generated, for other entities, the master picture is used as preview (if available).
|
||||
*/
|
||||
#[AsTwigFunction("entity_thumbnail")]
|
||||
public function entityThumbnail(AttachmentContainingDBElement $element, string $filter_name = 'thumbnail_sm'): ?string
|
||||
{
|
||||
if ($element instanceof Part) {
|
||||
$preview_attachment = $this->partPreviewGenerator->getTablePreviewAttachment($element);
|
||||
} else { // For other entities, we just use the master picture as preview, if available
|
||||
$preview_attachment = $element->getMasterPictureAttachment();
|
||||
}
|
||||
|
||||
if ($preview_attachment === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, $filter_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the font-awesome icon type for the given file extension. Returns "file" if no specific icon is available.
|
||||
* Null is allowed for files withot extension
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue