From 03ec4a9f84ff80c4effd377be5db2b1085eadf76 Mon Sep 17 00:00:00 2001 From: jona Date: Sun, 22 Dec 2024 17:57:59 +0100 Subject: [PATCH] Generalized interpretation of format06 barcodes, added ids for mouser --- .../Barcodes/BarcodeRedirector.php | 49 +++++--- .../Barcodes/BarcodeScanHelper.php | 108 +++++++++++++----- .../Barcodes/VendorBarcodeScanResult.php | 5 +- 3 files changed, 116 insertions(+), 46 deletions(-) diff --git a/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php b/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php index f4896b3b..8a573321 100644 --- a/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php +++ b/src/Services/LabelSystem/Barcodes/BarcodeRedirector.php @@ -42,6 +42,7 @@ declare(strict_types=1); namespace App\Services\LabelSystem\Barcodes; use App\Entity\LabelSystem\LabelSupportedElement; +use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; use Doctrine\ORM\EntityManagerInterface; @@ -109,31 +110,51 @@ final class BarcodeRedirector /** * 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 the - * same manufacturer product number. Only returns the first matching part. + * 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(VendorBarcodeScanResult $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. - $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->vendor_part_number); - $results = $qb->getQuery()->getResult(); - if($results){ - return $results[0]; + if($barcodeScan->vendor_part_number) { + $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->vendor_part_number); + $results = $qb->getQuery()->getResult(); + if ($results) { + return $results[0]; + } + } + + if(!$barcodeScan->manufacturer_part_number){ + 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 there's barcodes that contain the vendor we could make this more robust, but at least Digikey doesn't. - $qb = $this->em->getRepository(Part::class)->createQueryBuilder('part'); - $qb->where($qb->expr()->like('LOWER(part.manufacturer_product_number)', 'LOWER(:mpn)')); - $qb->setParameter('mpn', $barcodeScan->manufacturer_part_number); - $results = $qb->getQuery()->getResult(); + //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->manufacturer_part_number); + + if($barcodeScan->manufacturer){ + $manufacturerQb = $this->em->getRepository(Manufacturer::class)->createQueryBuilder("manufacturer"); + $manufacturerQb->where($manufacturerQb->expr()->like("LOWER(manufacturer.name)", "LOWER(:manufacturer_name)")); + $manufacturerQb->setParameter("manufacturer_name", $barcodeScan->manufacturer); + $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]; } diff --git a/src/Services/LabelSystem/Barcodes/BarcodeScanHelper.php b/src/Services/LabelSystem/Barcodes/BarcodeScanHelper.php index 84c4870d..62c1f798 100644 --- a/src/Services/LabelSystem/Barcodes/BarcodeScanHelper.php +++ b/src/Services/LabelSystem/Barcodes/BarcodeScanHelper.php @@ -90,7 +90,7 @@ final class BarcodeScanHelper return $this->parseIPNBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); } if ($type === BarcodeSourceType::VENDOR) { - return $this->parseDigikeyBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); + return $this->parseFormat06Barcode($input) ?? throw new InvalidArgumentException('Could not parse barcode'); } //Null means auto and we try the different formats @@ -112,7 +112,7 @@ final class BarcodeScanHelper return $result; } - $result = $this->parseDigikeyBarcode($input); + $result = $this->parseFormat06Barcode($input); if ($result !== null) { return $result; } @@ -120,51 +120,99 @@ final class BarcodeScanHelper throw new InvalidArgumentException('Unknown barcode'); } - private function parseDigikeyBarcode(string $input): ?VendorBarcodeScanResult + + /** + * 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 { if(!str_starts_with($input, "[)>\u{1E}06\u{1D}")){ return null; } + if(str_ends_with($input, "\u{04}")){ + $input = substr($input, 0, -1); + } + $barcodeParts = explode("\u{1D}",$input); - if (count($barcodeParts) !== 16){ + //get rid of the Format 06 identifier + array_shift($barcodeParts); + if (count($barcodeParts) < 2){ return null; } + $fieldIds = [ - ['id' => '', 'name' => ''], - ['id' => 'P', 'name' => 'Customer Part Number'], - ['id' => '1P', 'name' => 'Supplier Part Number'], - ['id' => '30P','name' => 'Digikey Part Number'], - ['id' => 'K', 'name' => 'Purchase Order Part Number'], - ['id' => '1K', 'name' => 'Digikey Sales Order Number'], - ['id' => '10K','name' => 'Digikey Invoice Number'], - ['id' => '9D', 'name' => 'Date Code'], - ['id' => '1T', 'name' => 'Lot Code'], - ['id' => '11K','name' => 'Packing List Number'], - ['id' => '4L', 'name' => 'Country of Origin'], - ['id' => 'Q', 'name' => 'Quantity'], - ['id' => '11Z','name' => 'Label Type'], - ['id' => '12Z','name' => 'Part ID'], - ['id' => '13Z','name' => 'NA'], - ['id' => '20Z','name' => 'Padding'] + //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', ]; $results = []; - foreach ($barcodeParts as $index => $part) { - $fieldProps = $fieldIds[$index]; - if(!str_starts_with($part, $fieldProps['id'])){ + 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)) { return null; } - $results[$fieldProps['name']] = substr($part, strlen($fieldProps['id'])); + $meaning = $fieldIds[$matches[0]]; + $fieldValue = substr($part, strlen($matches[0])); + $results[$meaning] = $fieldValue; + + } + return $results; + } + + /** + * 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; } - return new VendorBarcodeScanResult( - 'Digikey', - $results['Supplier Part Number'], - $results['Digikey Part Number'], - $results['Date Code'], - $results['Quantity'] + 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, ); } diff --git a/src/Services/LabelSystem/Barcodes/VendorBarcodeScanResult.php b/src/Services/LabelSystem/Barcodes/VendorBarcodeScanResult.php index a546289d..fc8cd2a3 100644 --- a/src/Services/LabelSystem/Barcodes/VendorBarcodeScanResult.php +++ b/src/Services/LabelSystem/Barcodes/VendorBarcodeScanResult.php @@ -31,11 +31,12 @@ namespace App\Services\LabelSystem\Barcodes; class VendorBarcodeScanResult { public function __construct( - public readonly string $vendor, + public readonly ?string $vendor = null, public readonly ?string $manufacturer_part_number = null, public readonly ?string $vendor_part_number = null, public readonly ?string $date_code = null, - public readonly ?string $quantity = null + public readonly ?string $quantity = null, + public readonly ?string $manufacturer = null ) { }