Skip to content

Commit 991daf0

Browse files
alufersjbtronics
andauthored
Implement parsing of TME QR codes (#1324)
* Implement parsing of TME QR codes They are present on parts purchased on tme.eu. It's based on the LCSC parser. Some older codes I found are in upper-case so I handle those too. * Removed unused method * Fixed translation message keys * Try to find TME part via SPN --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
1 parent 34a84bc commit 991daf0

File tree

7 files changed

+317
-1
lines changed

7 files changed

+317
-1
lines changed

src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ public function scanBarcodeContent(string $input, ?BarcodeSourceType $type = nul
105105
return new AmazonBarcodeScanResult($input);
106106
}
107107

108+
if ($type === BarcodeSourceType::TME) {
109+
return TMEBarcodeScanResult::parse($input);
110+
}
111+
108112
//Null means auto and we try the different formats
109113
$result = $this->parseInternalBarcode($input);
110114

@@ -144,6 +148,11 @@ public function scanBarcodeContent(string $input, ?BarcodeSourceType $type = nul
144148
return new AmazonBarcodeScanResult($input);
145149
}
146150

151+
// Try TME barcode
152+
if (TMEBarcodeScanResult::isTMEBarcode($input)) {
153+
return TMEBarcodeScanResult::parse($input);
154+
}
155+
147156
throw new InvalidArgumentException('Unknown barcode');
148157
}
149158

@@ -162,6 +171,7 @@ private function parseLCSCBarcode(string $input): LCSCBarcodeScanResult
162171
return LCSCBarcodeScanResult::parse($input);
163172
}
164173

174+
165175
private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult
166176
{
167177
$lot_repo = $this->entityManager->getRepository(PartLot::class);

src/Services/LabelSystem/BarcodeScanner/BarcodeScanResultHandler.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ public function resolveEntity(BarcodeScanResultInterface $barcodeScan): Part|Par
150150
?? $this->em->getRepository(Part::class)->getPartBySPN($barcodeScan->asin);
151151
}
152152

153+
if ($barcodeScan instanceof TMEBarcodeScanResult) {
154+
return $this->resolvePartFromTME($barcodeScan);
155+
}
156+
153157
return null;
154158
}
155159

@@ -236,6 +240,26 @@ private function resolvePartFromLCSC(LCSCBarcodeScanResult $barcodeScan): ?Part
236240
}
237241

238242

243+
private function resolvePartFromTME(TMEBarcodeScanResult $barcodeScan): ?Part
244+
{
245+
$pn = $barcodeScan->tmePartNumber;
246+
if ($pn) {
247+
$part = $this->em->getRepository(Part::class)->getPartByProviderInfo($pn);
248+
if ($part !== null) {
249+
return $part;
250+
}
251+
252+
//Try to find the part by SPN/SKU
253+
$part = $this->em->getRepository(Part::class)->getPartBySPN($pn);
254+
if ($part !== null) {
255+
return $part;
256+
}
257+
}
258+
259+
// Fallback: search by MPN
260+
return $this->em->getRepository(Part::class)->getPartByMPN($barcodeScan->mpn, $barcodeScan->manufacturer);
261+
}
262+
239263
/**
240264
* Tries to extract creation information for a part from the given barcode scan result. This can be used to
241265
* automatically fill in the info provider reference of a part, when creating a new part based on the scan result.
@@ -247,6 +271,20 @@ private function resolvePartFromLCSC(LCSCBarcodeScanResult $barcodeScan): ?Part
247271
*/
248272
public function getCreateInfos(BarcodeScanResultInterface $scanResult): ?array
249273
{
274+
// TME
275+
if ($scanResult instanceof TMEBarcodeScanResult) {
276+
if ($scanResult->tmePartNumber === null) {
277+
return null;
278+
}
279+
return [
280+
'providerKey' => 'tme',
281+
'providerId' => $scanResult->tmePartNumber,
282+
'lotAmount' => $scanResult->quantity,
283+
'lotName' => $scanResult->purchaseOrder,
284+
'lotUserBarcode' => $scanResult->rawInput,
285+
];
286+
}
287+
250288
// LCSC
251289
if ($scanResult instanceof LCSCBarcodeScanResult) {
252290
return [

src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,7 @@ enum BarcodeSourceType: string
5252
case LCSC = 'lcsc';
5353

5454
case AMAZON = 'amazon';
55+
56+
/** For TME (tme.eu) formatted QR codes */
57+
case TME = 'tme';
5558
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace App\Services\LabelSystem\BarcodeScanner;
24+
25+
use InvalidArgumentException;
26+
27+
/**
28+
* This class represents the content of a tme.eu barcode label.
29+
* The format is space-separated KEY:VALUE tokens, e.g.:
30+
* QTY:1000 PN:SMD0603-5K1-1% PO:32723349/7 MFR:ROYALOHM MPN:0603SAF5101T5E CoO:TH RoHS https://www.tme.eu/details/...
31+
*/
32+
readonly class TMEBarcodeScanResult implements BarcodeScanResultInterface
33+
{
34+
/** @var int|null Quantity (QTY) */
35+
public ?int $quantity;
36+
37+
/** @var string|null TME part number (PN) */
38+
public ?string $tmePartNumber;
39+
40+
/** @var string|null Purchase order number (PO) */
41+
public ?string $purchaseOrder;
42+
43+
/** @var string|null Manufacturer name (MFR) */
44+
public ?string $manufacturer;
45+
46+
/** @var string|null Manufacturer part number (MPN) */
47+
public ?string $mpn;
48+
49+
/** @var string|null Country of origin (CoO) */
50+
public ?string $countryOfOrigin;
51+
52+
/** @var bool Whether the part is RoHS compliant */
53+
public bool $rohs;
54+
55+
/** @var string|null The product URL */
56+
public ?string $productUrl;
57+
58+
/**
59+
* @param array<string, string> $fields Parsed key-value fields (keys uppercased)
60+
* @param string $rawInput Original barcode string
61+
*/
62+
public function __construct(
63+
public array $fields,
64+
public string $rawInput,
65+
) {
66+
$this->quantity = isset($this->fields['QTY']) ? (int) $this->fields['QTY'] : null;
67+
$this->tmePartNumber = $this->fields['PN'] ?? null;
68+
$this->purchaseOrder = $this->fields['PO'] ?? null;
69+
$this->manufacturer = $this->fields['MFR'] ?? null;
70+
$this->mpn = $this->fields['MPN'] ?? null;
71+
$this->countryOfOrigin = $this->fields['COO'] ?? null;
72+
$this->rohs = isset($this->fields['ROHS']);
73+
$this->productUrl = $this->fields['URL'] ?? null;
74+
}
75+
76+
public function getSourceType(): BarcodeSourceType
77+
{
78+
return BarcodeSourceType::TME;
79+
}
80+
81+
public function getDecodedForInfoMode(): array
82+
{
83+
return [
84+
'Barcode type' => 'TME',
85+
'TME Part No. (PN)' => $this->tmePartNumber ?? '',
86+
'MPN' => $this->mpn ?? '',
87+
'Manufacturer (MFR)' => $this->manufacturer ?? '',
88+
'Qty' => $this->quantity !== null ? (string) $this->quantity : '',
89+
'Purchase Order (PO)' => $this->purchaseOrder ?? '',
90+
'Country of Origin (CoO)' => $this->countryOfOrigin ?? '',
91+
'RoHS' => $this->rohs ? 'Yes' : 'No',
92+
'URL' => $this->productUrl ?? '',
93+
];
94+
}
95+
96+
/**
97+
* Returns true if the input looks like a TME barcode label (contains tme.eu URL).
98+
*/
99+
public static function isTMEBarcode(string $input): bool
100+
{
101+
return str_contains(strtolower($input), 'tme.eu');
102+
}
103+
104+
/**
105+
* Parse the TME barcode string into a TMEBarcodeScanResult.
106+
*/
107+
public static function parse(string $input): self
108+
{
109+
$raw = trim($input);
110+
111+
if (!self::isTMEBarcode($raw)) {
112+
throw new InvalidArgumentException('Not a TME barcode');
113+
}
114+
115+
$fields = [];
116+
117+
// Split on whitespace; each token is either KEY:VALUE, a bare keyword, or the URL
118+
$tokens = preg_split('/\s+/', $raw);
119+
foreach ($tokens as $token) {
120+
if ($token === '') {
121+
continue;
122+
}
123+
124+
// The TME URL
125+
if (str_starts_with(strtolower($token), 'http')) {
126+
$fields['URL'] = $token;
127+
continue;
128+
}
129+
130+
$colonPos = strpos($token, ':');
131+
if ($colonPos !== false) {
132+
$key = strtoupper(substr($token, 0, $colonPos));
133+
$value = substr($token, $colonPos + 1);
134+
$fields[$key] = $value;
135+
} else {
136+
// Bare keyword like "RoHS"
137+
$fields[strtoupper($token)] = '';
138+
}
139+
}
140+
141+
return new self($fields, $raw);
142+
}
143+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
/*
3+
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4+
*
5+
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
namespace App\Tests\Services\LabelSystem\BarcodeScanner;
22+
23+
use App\Services\LabelSystem\BarcodeScanner\BarcodeSourceType;
24+
use App\Services\LabelSystem\BarcodeScanner\TMEBarcodeScanResult;
25+
use InvalidArgumentException;
26+
use PHPUnit\Framework\TestCase;
27+
28+
class TMEBarcodeScanResultTest extends TestCase
29+
{
30+
private const EXAMPLE1 = 'QTY:1000 PN:SMD0603-5K1-1% PO:32723349/7 MFR:ROYALOHM MPN:0603SAF5101T5E CoO:TH RoHS https://www.tme.eu/details/SMD0603-5K1-1%25';
31+
private const EXAMPLE2 = 'QTY:5 PN:ETQP3M6R8KVP PO:31199729/3 MFR:PANASONIC MPN:ETQP3M6R8KVP RoHS https://www.tme.eu/details/ETQP3M6R8KVP';
32+
33+
public function testIsTMEBarcode(): void
34+
{
35+
$this->assertFalse(TMEBarcodeScanResult::isTMEBarcode('invalid'));
36+
$this->assertFalse(TMEBarcodeScanResult::isTMEBarcode('QTY:5 PN:ABC MPN:XYZ'));
37+
$this->assertFalse(TMEBarcodeScanResult::isTMEBarcode(''));
38+
39+
$this->assertTrue(TMEBarcodeScanResult::isTMEBarcode(self::EXAMPLE1));
40+
$this->assertTrue(TMEBarcodeScanResult::isTMEBarcode(self::EXAMPLE2));
41+
}
42+
43+
public function testParseInvalidThrows(): void
44+
{
45+
$this->expectException(InvalidArgumentException::class);
46+
TMEBarcodeScanResult::parse('not-a-tme-barcode');
47+
}
48+
49+
public function testParseExample1(): void
50+
{
51+
$scan = TMEBarcodeScanResult::parse(self::EXAMPLE1);
52+
53+
$this->assertSame(1000, $scan->quantity);
54+
$this->assertSame('SMD0603-5K1-1%', $scan->tmePartNumber);
55+
$this->assertSame('32723349/7', $scan->purchaseOrder);
56+
$this->assertSame('ROYALOHM', $scan->manufacturer);
57+
$this->assertSame('0603SAF5101T5E', $scan->mpn);
58+
$this->assertSame('TH', $scan->countryOfOrigin);
59+
$this->assertTrue($scan->rohs);
60+
$this->assertSame('https://www.tme.eu/details/SMD0603-5K1-1%25', $scan->productUrl);
61+
$this->assertSame(self::EXAMPLE1, $scan->rawInput);
62+
}
63+
64+
public function testParseExample2(): void
65+
{
66+
$scan = TMEBarcodeScanResult::parse(self::EXAMPLE2);
67+
68+
$this->assertSame(5, $scan->quantity);
69+
$this->assertSame('ETQP3M6R8KVP', $scan->tmePartNumber);
70+
$this->assertSame('31199729/3', $scan->purchaseOrder);
71+
$this->assertSame('PANASONIC', $scan->manufacturer);
72+
$this->assertSame('ETQP3M6R8KVP', $scan->mpn);
73+
$this->assertNull($scan->countryOfOrigin);
74+
$this->assertTrue($scan->rohs);
75+
$this->assertSame('https://www.tme.eu/details/ETQP3M6R8KVP', $scan->productUrl);
76+
}
77+
78+
public function testGetSourceType(): void
79+
{
80+
$scan = TMEBarcodeScanResult::parse(self::EXAMPLE2);
81+
$this->assertSame(BarcodeSourceType::TME, $scan->getSourceType());
82+
}
83+
84+
public function testParseUppercaseUrl(): void
85+
{
86+
$input = 'QTY:500 PN:M0.6W-10K MFR:ROYAL.OHM MPN:MF006FF1002A50 PO:7792659/8 HTTPS://WWW.TME.EU/DETAILS/M0.6W-10K';
87+
$this->assertTrue(TMEBarcodeScanResult::isTMEBarcode($input));
88+
89+
$scan = TMEBarcodeScanResult::parse($input);
90+
$this->assertSame(500, $scan->quantity);
91+
$this->assertSame('M0.6W-10K', $scan->tmePartNumber);
92+
$this->assertSame('ROYAL.OHM', $scan->manufacturer);
93+
$this->assertSame('MF006FF1002A50', $scan->mpn);
94+
$this->assertSame('7792659/8', $scan->purchaseOrder);
95+
$this->assertSame('HTTPS://WWW.TME.EU/DETAILS/M0.6W-10K', $scan->productUrl);
96+
}
97+
98+
public function testGetDecodedForInfoMode(): void
99+
{
100+
$scan = TMEBarcodeScanResult::parse(self::EXAMPLE1);
101+
$decoded = $scan->getDecodedForInfoMode();
102+
103+
$this->assertSame('TME', $decoded['Barcode type']);
104+
$this->assertSame('SMD0603-5K1-1%', $decoded['TME Part No. (PN)']);
105+
$this->assertSame('0603SAF5101T5E', $decoded['MPN']);
106+
$this->assertSame('ROYALOHM', $decoded['Manufacturer (MFR)']);
107+
$this->assertSame('1000', $decoded['Qty']);
108+
$this->assertSame('Yes', $decoded['RoHS']);
109+
}
110+
}

translations/messages.de.xlf

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de">
3-
<file id="messages.en">
3+
<file id="messages.de">
44
<unit id="x_wTSQS" name="attachment_type.caption">
55
<segment state="translated">
66
<source>attachment_type.caption</source>
@@ -12861,6 +12861,12 @@ Buerklin-API-Authentication-Server:
1286112861
<target>Amazon Barcode</target>
1286212862
</segment>
1286312863
</unit>
12864+
<unit id="d.V2Pid" name="scan_dialog.mode.tme">
12865+
<segment state="translated">
12866+
<source>scan_dialog.mode.tme</source>
12867+
<target>TME Barcode</target>
12868+
</segment>
12869+
</unit>
1286412870
<unit id="BQWuR_G" name="settings.ips.canopy">
1286512871
<segment state="translated">
1286612872
<source>settings.ips.canopy</source>

translations/messages.en.xlf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12863,6 +12863,12 @@ Buerklin-API Authentication server:
1286312863
<target>Amazon barcode</target>
1286412864
</segment>
1286512865
</unit>
12866+
<unit id="d.V2Pid" name="scan_dialog.mode.tme">
12867+
<segment state="translated">
12868+
<source>scan_dialog.mode.tme</source>
12869+
<target>TME barcode</target>
12870+
</segment>
12871+
</unit>
1286612872
<unit id="BQWuR_G" name="settings.ips.canopy">
1286712873
<segment state="translated">
1286812874
<source>settings.ips.canopy</source>

0 commit comments

Comments
 (0)