2020-04-26 21:26:11 +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-26 21:26:11 +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-26 21:26:11 +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\Services\LabelSystem\Barcodes;
|
|
|
|
|
|
2023-10-26 22:23:43 +02:00
|
|
|
use App\Entity\LabelSystem\LabelSupportedElement;
|
2023-11-12 00:36:13 +01:00
|
|
|
use App\Entity\Parts\Part;
|
2023-11-17 23:23:54 +01:00
|
|
|
use App\Entity\Parts\PartLot;
|
2023-11-12 00:36:13 +01:00
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
2022-08-14 19:32:53 +02:00
|
|
|
use InvalidArgumentException;
|
|
|
|
|
|
2023-06-11 15:02:59 +02:00
|
|
|
/**
|
2024-06-22 00:31:43 +02:00
|
|
|
* @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeScanHelperTest
|
2023-06-11 15:02:59 +02:00
|
|
|
*/
|
2023-10-26 22:23:43 +02:00
|
|
|
final class BarcodeScanHelper
|
2020-04-26 21:26:11 +02:00
|
|
|
{
|
2020-05-09 18:17:23 +02:00
|
|
|
private const PREFIX_TYPE_MAP = [
|
2023-10-26 22:23:43 +02:00
|
|
|
'L' => LabelSupportedElement::PART_LOT,
|
|
|
|
|
'P' => LabelSupportedElement::PART,
|
|
|
|
|
'S' => LabelSupportedElement::STORELOCATION,
|
2020-04-26 21:26:11 +02:00
|
|
|
];
|
|
|
|
|
|
2023-10-26 22:23:43 +02:00
|
|
|
public const QR_TYPE_MAP = [
|
|
|
|
|
'lot' => LabelSupportedElement::PART_LOT,
|
|
|
|
|
'part' => LabelSupportedElement::PART,
|
|
|
|
|
'location' => LabelSupportedElement::STORELOCATION,
|
|
|
|
|
];
|
|
|
|
|
|
2023-11-12 00:36:13 +01:00
|
|
|
public function __construct(private readonly EntityManagerInterface $entityManager)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-26 22:23:43 +02:00
|
|
|
/**
|
|
|
|
|
* Parse the given barcode content and return the target type and ID.
|
|
|
|
|
* If the barcode could not be parsed, an exception is thrown.
|
|
|
|
|
* Using the $type parameter, you can specify how the barcode should be parsed. If set to null, the function
|
|
|
|
|
* will try to guess the type.
|
|
|
|
|
* @param string $input
|
|
|
|
|
* @param BarcodeSourceType|null $type
|
2024-12-20 11:05:22 +01:00
|
|
|
* @return LocalBarcodeScanResult
|
2023-10-26 22:23:43 +02:00
|
|
|
*/
|
2024-12-20 11:05:22 +01:00
|
|
|
public function scanBarcodeContent(string $input, ?BarcodeSourceType $type = null): LocalBarcodeScanResult | VendorBarcodeScanResult
|
2023-10-26 22:23:43 +02:00
|
|
|
{
|
|
|
|
|
//Do specific parsing
|
|
|
|
|
if ($type === BarcodeSourceType::INTERNAL) {
|
|
|
|
|
return $this->parseInternalBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
|
|
|
|
|
}
|
2024-12-20 11:05:22 +01:00
|
|
|
if ($type === BarcodeSourceType::USER_DEFINED) {
|
|
|
|
|
return $this->parseUserDefinedBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
|
2023-11-17 23:23:54 +01:00
|
|
|
}
|
2023-11-12 00:36:13 +01:00
|
|
|
if ($type === BarcodeSourceType::IPN) {
|
|
|
|
|
return $this->parseIPNBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
|
|
|
|
|
}
|
2024-12-20 11:05:22 +01:00
|
|
|
if ($type === BarcodeSourceType::VENDOR) {
|
2024-12-22 17:57:59 +01:00
|
|
|
return $this->parseFormat06Barcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
|
2024-12-20 11:05:22 +01:00
|
|
|
}
|
2023-10-26 22:23:43 +02:00
|
|
|
|
|
|
|
|
//Null means auto and we try the different formats
|
|
|
|
|
$result = $this->parseInternalBarcode($input);
|
|
|
|
|
|
|
|
|
|
if ($result !== null) {
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
2023-11-12 00:36:13 +01:00
|
|
|
|
2024-12-20 11:05:22 +01:00
|
|
|
//Try to parse as User defined barcode
|
|
|
|
|
$result = $this->parseUserDefinedBarcode($input);
|
2023-11-17 23:23:54 +01:00
|
|
|
if ($result !== null) {
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 00:36:13 +01:00
|
|
|
//Try to parse as IPN barcode
|
|
|
|
|
$result = $this->parseIPNBarcode($input);
|
|
|
|
|
if ($result !== null) {
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-22 17:57:59 +01:00
|
|
|
$result = $this->parseFormat06Barcode($input);
|
2024-12-20 11:05:22 +01:00
|
|
|
if ($result !== null) {
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-17 23:23:54 +01:00
|
|
|
throw new InvalidArgumentException('Unknown barcode');
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-22 17:57:59 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parses Format 06 Barcodes according to ISO/IEC 15434. That standard calls on ASC MH10 to specify
|
|
|
|
|
* the data identifiers, but these are way too many to incorporate here. EIGP 114.2018 is yet another standard
|
|
|
|
|
* based on Format 06 which specifies identifiers for the electronics industry. I've included the identifiers
|
|
|
|
|
* from that standard, plus the extra ones I found on Digikey and Mouser Bags.
|
|
|
|
|
* @param string $input what was read from the barcode
|
|
|
|
|
* @return ?array Array of the form ["Meaning" => "Value"]
|
|
|
|
|
*/
|
|
|
|
|
private function decodeFormat06Barcode(string $input): ?array
|
2024-12-20 11:05:22 +01:00
|
|
|
{
|
2024-12-22 17:23:43 +01:00
|
|
|
if(!str_starts_with($input, "[)>\u{1E}06\u{1D}")){
|
2024-12-20 11:05:22 +01:00
|
|
|
return null;
|
|
|
|
|
}
|
2024-12-22 17:57:59 +01:00
|
|
|
if(str_ends_with($input, "\u{04}")){
|
|
|
|
|
$input = substr($input, 0, -1);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-22 17:23:43 +01:00
|
|
|
$barcodeParts = explode("\u{1D}",$input);
|
2024-12-22 17:57:59 +01:00
|
|
|
//get rid of the Format 06 identifier
|
|
|
|
|
array_shift($barcodeParts);
|
|
|
|
|
if (count($barcodeParts) < 2){
|
2024-12-20 11:05:22 +01:00
|
|
|
return null;
|
|
|
|
|
}
|
2024-12-22 17:57:59 +01:00
|
|
|
|
2024-12-20 11:05:22 +01:00
|
|
|
$fieldIds = [
|
2024-12-22 17:57:59 +01:00
|
|
|
//IDs per EIGP 114.2018
|
|
|
|
|
'6D' => 'Ship Date',
|
|
|
|
|
'P' => 'Customer Part Number',
|
|
|
|
|
'1P' => 'Supplier Part Number',
|
|
|
|
|
'Q' => 'Quantity',
|
|
|
|
|
'K' => 'Purchase Order Part Number',
|
|
|
|
|
'4K' => 'Purchase Order Line Number',
|
|
|
|
|
'9D' => 'Date Code',
|
|
|
|
|
'10D' => 'Alternative Date Code',
|
|
|
|
|
'1T' => 'Lot Code',
|
|
|
|
|
'4L' => 'Country of Origin',
|
|
|
|
|
'3S' => 'Package ID 1',
|
|
|
|
|
'4S' => 'Package ID 2',
|
|
|
|
|
'5S' => 'Package ID 3',
|
|
|
|
|
'11K' => 'Packing List Number',
|
|
|
|
|
'S' => 'Serial Number',
|
|
|
|
|
'33P' => 'BIN Code',
|
|
|
|
|
'13Q' => 'Package Count',
|
|
|
|
|
'2P' => 'Revision Number',
|
|
|
|
|
//IDs used by Digikey
|
|
|
|
|
'30P' => 'Digikey Part Number',
|
|
|
|
|
'1K' => 'Sales Order Number',
|
|
|
|
|
'10K' => 'Invoice Number',
|
|
|
|
|
'11Z' => 'Label Type',
|
|
|
|
|
'12Z' => 'Part ID',
|
|
|
|
|
'13Z' => 'NA',
|
|
|
|
|
'20Z' => 'Padding',
|
|
|
|
|
//IDs used by Mouser
|
|
|
|
|
'14K' => 'Position in Order',
|
|
|
|
|
'1V' => 'Manufacturer',
|
2024-12-20 11:05:22 +01:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$results = [];
|
|
|
|
|
|
2024-12-22 17:57:59 +01:00
|
|
|
foreach($barcodeParts as $part) {
|
|
|
|
|
//^ 0* ([1-9]? \d* [A-Z])
|
|
|
|
|
//Start of the string Leading zeros are discarded Not a zero Any number of digits single uppercase Letter
|
|
|
|
|
// 00 1 4 K
|
|
|
|
|
|
|
|
|
|
if(!preg_match('/^0*([1-9]?\d*[A-Z])/', $part, $matches)) {
|
2024-12-20 11:05:22 +01:00
|
|
|
return null;
|
|
|
|
|
}
|
2024-12-22 17:57:59 +01:00
|
|
|
$meaning = $fieldIds[$matches[0]];
|
|
|
|
|
$fieldValue = substr($part, strlen($matches[0]));
|
|
|
|
|
$results[$meaning] = $fieldValue;
|
|
|
|
|
|
2024-12-20 11:05:22 +01:00
|
|
|
}
|
2024-12-22 17:57:59 +01:00
|
|
|
return $results;
|
|
|
|
|
}
|
2024-12-20 11:05:22 +01:00
|
|
|
|
2024-12-22 17:57:59 +01:00
|
|
|
/**
|
|
|
|
|
* Decodes a Format06 Barcode and puts it into a VendorBarcodeScanResult
|
|
|
|
|
* See decodeFormat06Barcode for details
|
|
|
|
|
*/
|
|
|
|
|
private function parseFormat06Barcode(string $input): ?VendorBarcodeScanResult{
|
|
|
|
|
$results = $this->decodeFormat06Barcode($input);
|
|
|
|
|
|
|
|
|
|
if($results === null){
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2024-12-20 11:05:22 +01:00
|
|
|
|
|
|
|
|
return new VendorBarcodeScanResult(
|
2024-12-22 17:57:59 +01:00
|
|
|
manufacturer_part_number: $results['Supplier Part Number'] ?? null,
|
|
|
|
|
vendor_part_number: $results['Digikey Part Number'] ?? null,
|
|
|
|
|
date_code: $results['Date Code'] ?? null,
|
|
|
|
|
quantity: $results['Quantity'] ?? null,
|
|
|
|
|
manufacturer: $results['Manufacturer'] ?? null,
|
2024-12-20 11:05:22 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult
|
2023-11-17 23:23:54 +01:00
|
|
|
{
|
|
|
|
|
$lot_repo = $this->entityManager->getRepository(PartLot::class);
|
|
|
|
|
//Find only the first result
|
2025-01-03 20:55:55 +01:00
|
|
|
$results = $lot_repo->findBy(['user_barcode' => $input], limit: 1);
|
2023-11-17 23:23:54 +01:00
|
|
|
|
|
|
|
|
if (count($results) === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
//We found a part, so use it to create the result
|
|
|
|
|
$lot = $results[0];
|
|
|
|
|
|
2024-12-20 11:05:22 +01:00
|
|
|
return new LocalBarcodeScanResult(
|
2023-11-17 23:23:54 +01:00
|
|
|
target_type: LabelSupportedElement::PART_LOT,
|
|
|
|
|
target_id: $lot->getID(),
|
2024-12-20 11:05:22 +01:00
|
|
|
source_type: BarcodeSourceType::USER_DEFINED
|
2023-11-17 23:23:54 +01:00
|
|
|
);
|
2023-10-26 22:23:43 +02:00
|
|
|
}
|
|
|
|
|
|
2024-12-20 11:05:22 +01:00
|
|
|
private function parseIPNBarcode(string $input): ?LocalBarcodeScanResult
|
2023-11-12 00:36:13 +01:00
|
|
|
{
|
|
|
|
|
$part_repo = $this->entityManager->getRepository(Part::class);
|
|
|
|
|
//Find only the first result
|
|
|
|
|
$results = $part_repo->findBy(['ipn' => $input], limit: 1);
|
|
|
|
|
|
|
|
|
|
if (count($results) === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
//We found a part, so use it to create the result
|
|
|
|
|
$part = $results[0];
|
|
|
|
|
|
2024-12-20 11:05:22 +01:00
|
|
|
return new LocalBarcodeScanResult(
|
2023-11-12 00:36:13 +01:00
|
|
|
target_type: LabelSupportedElement::PART,
|
|
|
|
|
target_id: $part->getID(),
|
|
|
|
|
source_type: BarcodeSourceType::IPN
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-26 21:26:11 +02:00
|
|
|
/**
|
2023-10-26 22:23:43 +02:00
|
|
|
* This function tries to interpret the given barcode content as an internal barcode.
|
|
|
|
|
* If the barcode could not be parsed at all, null is returned. If the barcode is a valid format, but could
|
|
|
|
|
* not be found in the database, an exception is thrown.
|
|
|
|
|
* @param string $input
|
2024-12-20 11:05:22 +01:00
|
|
|
* @return LocalBarcodeScanResult|null
|
2020-04-26 21:26:11 +02:00
|
|
|
*/
|
2024-12-20 11:05:22 +01:00
|
|
|
private function parseInternalBarcode(string $input): ?LocalBarcodeScanResult
|
2020-04-26 21:26:11 +02:00
|
|
|
{
|
|
|
|
|
$input = trim($input);
|
|
|
|
|
$matches = [];
|
|
|
|
|
|
|
|
|
|
//Some scanner output '-' as ß, so replace it (ß is never used, so we can replace it safely)
|
|
|
|
|
$input = str_replace('ß', '-', $input);
|
|
|
|
|
|
|
|
|
|
//Extract parts from QR code's URL
|
|
|
|
|
if (preg_match('#^https?://.*/scan/(\w+)/(\d+)/?$#', $input, $matches)) {
|
2024-12-20 11:05:22 +01:00
|
|
|
return new LocalBarcodeScanResult(
|
2023-10-26 22:23:43 +02:00
|
|
|
target_type: self::QR_TYPE_MAP[strtolower($matches[1])],
|
|
|
|
|
target_id: (int) $matches[2],
|
|
|
|
|
source_type: BarcodeSourceType::INTERNAL
|
|
|
|
|
);
|
2020-04-26 21:26:11 +02:00
|
|
|
}
|
|
|
|
|
|
2020-05-07 23:11:50 +02:00
|
|
|
//New Code39 barcode use L0001 format
|
|
|
|
|
if (preg_match('#^([A-Z])(\d{4,})$#', $input, $matches)) {
|
|
|
|
|
$prefix = $matches[1];
|
|
|
|
|
$id = (int) $matches[2];
|
|
|
|
|
|
2020-08-21 21:36:22 +02:00
|
|
|
if (!isset(self::PREFIX_TYPE_MAP[$prefix])) {
|
2022-08-14 19:32:53 +02:00
|
|
|
throw new InvalidArgumentException('Unknown prefix '.$prefix);
|
2020-05-07 23:11:50 +02:00
|
|
|
}
|
2020-05-10 21:39:31 +02:00
|
|
|
|
2024-12-20 11:05:22 +01:00
|
|
|
return new LocalBarcodeScanResult(
|
2023-10-26 22:23:43 +02:00
|
|
|
target_type: self::PREFIX_TYPE_MAP[$prefix],
|
|
|
|
|
target_id: $id,
|
|
|
|
|
source_type: BarcodeSourceType::INTERNAL
|
|
|
|
|
);
|
2020-05-07 23:11:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//During development the L-000001 format was used
|
2020-04-26 21:26:11 +02:00
|
|
|
if (preg_match('#^(\w)-(\d{6,})$#', $input, $matches)) {
|
|
|
|
|
$prefix = $matches[1];
|
|
|
|
|
$id = (int) $matches[2];
|
|
|
|
|
|
2020-08-21 21:36:22 +02:00
|
|
|
if (!isset(self::PREFIX_TYPE_MAP[$prefix])) {
|
2022-08-14 19:32:53 +02:00
|
|
|
throw new InvalidArgumentException('Unknown prefix '.$prefix);
|
2020-04-26 21:26:11 +02:00
|
|
|
}
|
2020-05-10 21:39:31 +02:00
|
|
|
|
2024-12-20 11:05:22 +01:00
|
|
|
return new LocalBarcodeScanResult(
|
2023-10-26 22:23:43 +02:00
|
|
|
target_type: self::PREFIX_TYPE_MAP[$prefix],
|
|
|
|
|
target_id: $id,
|
|
|
|
|
source_type: BarcodeSourceType::INTERNAL
|
|
|
|
|
);
|
2020-04-26 21:26:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Legacy Part-DB location labels used $L00336 format
|
|
|
|
|
if (preg_match('#^\$L(\d{5,})$#', $input, $matches)) {
|
2024-12-20 11:05:22 +01:00
|
|
|
return new LocalBarcodeScanResult(
|
2023-10-26 22:23:43 +02:00
|
|
|
target_type: LabelSupportedElement::STORELOCATION,
|
|
|
|
|
target_id: (int) $matches[1],
|
|
|
|
|
source_type: BarcodeSourceType::INTERNAL
|
|
|
|
|
);
|
2020-04-26 21:26:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Legacy Part-DB used EAN8 barcodes for part labels. Format 0000001(2) (note the optional 8th digit => checksum)
|
|
|
|
|
if (preg_match('#^(\d{7})\d?$#', $input, $matches)) {
|
2024-12-20 11:05:22 +01:00
|
|
|
return new LocalBarcodeScanResult(
|
2023-10-26 22:23:43 +02:00
|
|
|
target_type: LabelSupportedElement::PART,
|
|
|
|
|
target_id: (int) $matches[1],
|
|
|
|
|
source_type: BarcodeSourceType::INTERNAL
|
|
|
|
|
);
|
2020-04-26 21:26:11 +02:00
|
|
|
}
|
|
|
|
|
|
2023-10-26 22:23:43 +02:00
|
|
|
//This function abstain from further parsing
|
|
|
|
|
return null;
|
2020-04-26 21:26:11 +02:00
|
|
|
}
|
2020-05-10 21:39:31 +02:00
|
|
|
}
|