mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-11 15:34:15 +00:00
Refactoring supplier barcode support (#5922)
* Refactoring supplier barcode support - Add a set of standard field name strings - Map from custom fields to standard fields - Helper functions for returning common field data - Updated unit tests * Update unit tests * Fix unit test * Add more unit tests' * Improve error messages
This commit is contained in:
@ -6,7 +6,6 @@ This plugin can currently only match DigiKey barcodes to supplier parts.
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.base.barcodes.mixins import SupplierBarcodeData
|
||||
from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
|
||||
|
||||
|
||||
@ -20,6 +19,7 @@ class DigiKeyPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
||||
AUTHOR = _("InvenTree contributors")
|
||||
|
||||
DEFAULT_SUPPLIER_NAME = "DigiKey"
|
||||
|
||||
SETTINGS = {
|
||||
"SUPPLIER_ID": {
|
||||
"name": _("Supplier"),
|
||||
@ -28,22 +28,7 @@ class DigiKeyPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
||||
}
|
||||
}
|
||||
|
||||
def parse_supplier_barcode_data(self, barcode_data):
|
||||
"""Get supplier_part and barcode_fields from DigiKey DataMatrix-Code."""
|
||||
|
||||
if not isinstance(barcode_data, str):
|
||||
return None
|
||||
|
||||
if not (barcode_fields := self.parse_ecia_barcode2d(barcode_data)):
|
||||
return None
|
||||
|
||||
# digikey barcodes should always contain a SKU
|
||||
if "supplier_part_number" not in barcode_fields:
|
||||
return None
|
||||
def extract_barcode_fields(self, barcode_data) -> dict[str, str]:
|
||||
"""Extract barcode fields from a DigiKey plugin"""
|
||||
|
||||
return SupplierBarcodeData(
|
||||
SKU=barcode_fields.get("supplier_part_number"),
|
||||
MPN=barcode_fields.get("manufacturer_part_number"),
|
||||
quantity=barcode_fields.get("quantity"),
|
||||
order_number=barcode_fields.get("purchase_order_number"),
|
||||
)
|
||||
return self.parse_ecia_barcode2d(barcode_data)
|
||||
|
@ -8,7 +8,6 @@ import re
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.base.barcodes.mixins import SupplierBarcodeData
|
||||
from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
|
||||
|
||||
|
||||
@ -30,23 +29,40 @@ class LCSCPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
||||
}
|
||||
}
|
||||
|
||||
def parse_supplier_barcode_data(self, barcode_data):
|
||||
"""Get supplier_part and barcode_fields from LCSC QR-Code."""
|
||||
LCSC_BARCODE_REGEX = re.compile(r"^{((?:[^:,]+:[^:,]*,)*(?:[^:,]+:[^:,]*))}$")
|
||||
|
||||
if not isinstance(barcode_data, str):
|
||||
return None
|
||||
# Custom field mapping for LCSC barcodes
|
||||
LCSC_FIELDS = {
|
||||
"pm": SupplierBarcodeMixin.MANUFACTURER_PART_NUMBER,
|
||||
"pc": SupplierBarcodeMixin.SUPPLIER_PART_NUMBER,
|
||||
"qty": SupplierBarcodeMixin.QUANTITY,
|
||||
"on": SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER,
|
||||
}
|
||||
|
||||
def extract_barcode_fields(self, barcode_data: str) -> dict[str, str]:
|
||||
"""Get supplier_part and barcode_fields from LCSC QR-Code.
|
||||
|
||||
if not (match := LCSC_BARCODE_REGEX.fullmatch(barcode_data)):
|
||||
return None
|
||||
Example LCSC QR-Code: {pbn:PICK2009291337,on:SO2009291337,pc:C312270}
|
||||
"""
|
||||
|
||||
barcode_fields = dict(pair.split(":") for pair in match.group(1).split(","))
|
||||
if not self.LCSC_BARCODE_REGEX.fullmatch(barcode_data):
|
||||
return {}
|
||||
|
||||
return SupplierBarcodeData(
|
||||
SKU=barcode_fields.get("pc"),
|
||||
MPN=barcode_fields.get("pm"),
|
||||
quantity=barcode_fields.get("qty"),
|
||||
order_number=barcode_fields.get("on"),
|
||||
# Extract fields
|
||||
fields = SupplierBarcodeMixin.split_fields(
|
||||
barcode_data,
|
||||
delimiter=',',
|
||||
header='{',
|
||||
trailer='}',
|
||||
)
|
||||
|
||||
fields = dict(pair.split(":") for pair in fields)
|
||||
|
||||
barcode_fields = {}
|
||||
|
||||
# Map from LCSC field names to standard field names
|
||||
for key, field in self.LCSC_FIELDS.items():
|
||||
if key in fields:
|
||||
barcode_fields[field] = fields[key]
|
||||
|
||||
LCSC_BARCODE_REGEX = re.compile(r"^{((?:[^:,]+:[^:,]*,)*(?:[^:,]+:[^:,]*))}$")
|
||||
return barcode_fields
|
||||
|
@ -6,7 +6,6 @@ This plugin currently only match Mouser barcodes to supplier parts.
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.base.barcodes.mixins import SupplierBarcodeData
|
||||
from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
|
||||
|
||||
|
||||
@ -28,18 +27,7 @@ class MouserPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
||||
}
|
||||
}
|
||||
|
||||
def parse_supplier_barcode_data(self, barcode_data):
|
||||
def extract_barcode_fields(self, barcode_data: str) -> dict[str, str]:
|
||||
"""Get supplier_part and barcode_fields from Mouser DataMatrix-Code."""
|
||||
|
||||
if not isinstance(barcode_data, str):
|
||||
return None
|
||||
|
||||
if not (barcode_fields := self.parse_ecia_barcode2d(barcode_data)):
|
||||
return None
|
||||
|
||||
return SupplierBarcodeData(
|
||||
SKU=barcode_fields.get("supplier_part_number"),
|
||||
MPN=barcode_fields.get("manufacturer_part_number"),
|
||||
quantity=barcode_fields.get("quantity"),
|
||||
order_number=barcode_fields.get("purchase_order_number"),
|
||||
)
|
||||
return self.parse_ecia_barcode2d(barcode_data)
|
||||
|
@ -12,6 +12,8 @@ from stock.models import StockItem, StockLocation
|
||||
class SupplierBarcodeTests(InvenTreeAPITestCase):
|
||||
"""Tests barcode parsing for all suppliers."""
|
||||
|
||||
SCAN_URL = reverse("api-barcode-scan")
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Create supplier parts for barcodes."""
|
||||
@ -41,76 +43,90 @@ class SupplierBarcodeTests(InvenTreeAPITestCase):
|
||||
SupplierPart.objects.bulk_create(supplier_parts)
|
||||
|
||||
def test_digikey_barcode(self):
|
||||
"""Test digikey barcode."""
|
||||
"""Test digikey barcode"""
|
||||
|
||||
url = reverse("api-barcode-scan")
|
||||
result = self.post(url, data={"barcode": DIGIKEY_BARCODE})
|
||||
result = self.post(self.SCAN_URL, data={"barcode": DIGIKEY_BARCODE}, expected_code=200)
|
||||
self.assertEqual(result.data['plugin'], 'DigiKeyPlugin')
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert "pk" in supplier_part_data
|
||||
self.assertIn('pk', supplier_part_data)
|
||||
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
self.assertEqual(supplier_part.SKU, "296-LM358BIDDFRCT-ND")
|
||||
|
||||
def test_digikey_2_barcode(self):
|
||||
"""Test digikey barcode which uses 30P instead of P"""
|
||||
result = self.post(self.SCAN_URL, data={"barcode": DIGIKEY_BARCODE_2}, expected_code=200)
|
||||
self.assertEqual(result.data['plugin'], 'DigiKeyPlugin')
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
self.assertIn('pk', supplier_part_data)
|
||||
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert supplier_part.SKU == "296-LM358BIDDFRCT-ND"
|
||||
self.assertEqual(supplier_part.SKU, "296-LM358BIDDFRCT-ND")
|
||||
|
||||
def test_digikey_3_barcode(self):
|
||||
"""Test digikey barcode which is invalid"""
|
||||
self.post(self.SCAN_URL, data={"barcode": DIGIKEY_BARCODE_3}, expected_code=400)
|
||||
|
||||
def test_mouser_barcode(self):
|
||||
"""Test mouser barcode with custom order number."""
|
||||
|
||||
url = reverse("api-barcode-scan")
|
||||
result = self.post(url, data={"barcode": MOUSER_BARCODE})
|
||||
result = self.post(self.SCAN_URL, data={"barcode": MOUSER_BARCODE}, expected_code=200)
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert "pk" in supplier_part_data
|
||||
self.assertIn('pk', supplier_part_data)
|
||||
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert supplier_part.SKU == "1"
|
||||
self.assertEqual(supplier_part.SKU, '1')
|
||||
|
||||
def test_old_mouser_barcode(self):
|
||||
"""Test old mouser barcode with messed up header."""
|
||||
|
||||
url = reverse("api-barcode-scan")
|
||||
result = self.post(url, data={"barcode": MOUSER_BARCODE_OLD})
|
||||
result = self.post(self.SCAN_URL, data={"barcode": MOUSER_BARCODE_OLD}, expected_code=200)
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert "pk" in supplier_part_data
|
||||
self.assertIn('pk', supplier_part_data)
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert supplier_part.SKU == "2"
|
||||
self.assertEqual(supplier_part.SKU, '2')
|
||||
|
||||
def test_lcsc_barcode(self):
|
||||
"""Test LCSC barcode."""
|
||||
|
||||
url = reverse("api-barcode-scan")
|
||||
result = self.post(url, data={"barcode": LCSC_BARCODE})
|
||||
result = self.post(self.SCAN_URL, data={"barcode": LCSC_BARCODE}, expected_code=200)
|
||||
|
||||
self.assertEqual(result.data['plugin'], 'LCSCPlugin')
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert supplier_part_data is not None
|
||||
self.assertIn('pk', supplier_part_data)
|
||||
|
||||
assert "pk" in supplier_part_data
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert supplier_part.SKU == "C312270"
|
||||
self.assertEqual(supplier_part.SKU, 'C312270')
|
||||
|
||||
def test_tme_qrcode(self):
|
||||
"""Test TME QR-Code."""
|
||||
|
||||
url = reverse("api-barcode-scan")
|
||||
result = self.post(url, data={"barcode": TME_QRCODE})
|
||||
result = self.post(self.SCAN_URL, data={"barcode": TME_QRCODE}, expected_code=200)
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert supplier_part_data is not None
|
||||
self.assertEqual(result.data['plugin'], 'TMEPlugin')
|
||||
|
||||
assert "pk" in supplier_part_data
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
self.assertIn('pk', supplier_part_data)
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert supplier_part.SKU == "WBP-302"
|
||||
self.assertEqual(supplier_part.SKU, 'WBP-302')
|
||||
|
||||
def test_tme_barcode2d(self):
|
||||
"""Test TME DataMatrix-Code."""
|
||||
|
||||
url = reverse("api-barcode-scan")
|
||||
result = self.post(url, data={"barcode": TME_DATAMATRIX_CODE})
|
||||
result = self.post(self.SCAN_URL, data={"barcode": TME_DATAMATRIX_CODE}, expected_code=200)
|
||||
|
||||
self.assertEqual(result.data['plugin'], 'TMEPlugin')
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert supplier_part_data is not None
|
||||
self.assertIn('pk', supplier_part_data)
|
||||
|
||||
assert "pk" in supplier_part_data
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert supplier_part.SKU == "WBP-302"
|
||||
self.assertEqual(supplier_part.SKU, 'WBP-302')
|
||||
|
||||
|
||||
class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
@ -161,7 +177,7 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
|
||||
result1 = self.post(url, data={"barcode": DIGIKEY_BARCODE})
|
||||
assert result1.status_code == 400
|
||||
assert result1.data["error"].startswith("Failed to find placed purchase order")
|
||||
assert result1.data["error"].startswith("No matching purchase order")
|
||||
|
||||
self.purchase_order1.place_order()
|
||||
|
||||
@ -273,9 +289,10 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
|
||||
url = reverse("api-barcode-po-receive")
|
||||
barcode = MOUSER_BARCODE.replace("\x1dQ3", "")
|
||||
result = self.post(url, data={"barcode": barcode})
|
||||
assert "lineitem" in result.data
|
||||
assert "quantity" not in result.data["lineitem"]
|
||||
response = self.post(url, data={"barcode": barcode}, expected_code=200)
|
||||
|
||||
assert "lineitem" in response.data
|
||||
assert "quantity" not in response.data["lineitem"]
|
||||
|
||||
|
||||
DIGIKEY_BARCODE = (
|
||||
@ -286,6 +303,25 @@ DIGIKEY_BARCODE = (
|
||||
"0000000000000000000000000000000000"
|
||||
)
|
||||
|
||||
# Uses 30P instead of P
|
||||
DIGIKEY_BARCODE_2 = (
|
||||
"[)>\x1e06\x1d30P296-LM358BIDDFRCT-ND\x1dK\x1d1K72991337\x1d"
|
||||
"10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337"
|
||||
"\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000"
|
||||
"00000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000"
|
||||
)
|
||||
|
||||
# Invalid code
|
||||
DIGIKEY_BARCODE_3 = (
|
||||
"[)>\x1e06\x1dPnonsense\x1d30Pnonsense\x1d1Pnonsense\x1dK\x1d1K72991337\x1d"
|
||||
"10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337"
|
||||
"\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000"
|
||||
"00000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000"
|
||||
|
||||
)
|
||||
|
||||
MOUSER_BARCODE = (
|
||||
"[)>\x1e06\x1dKP0-1337\x1d14K011\x1d1PMC34063ADR\x1dQ3\x1d11K073121337\x1d4"
|
||||
"LMX\x1d1VTI\x1e\x04"
|
||||
@ -305,4 +341,5 @@ TME_QRCODE = (
|
||||
"QTY:1 PN:WBP-302 PO:19361337/1 CPO:PO-2023-06-08-001337 MFR:WISHERENTERPRI"
|
||||
"SE MPN:WBP-302 RoHS https://www.tme.eu/details/WBP-302"
|
||||
)
|
||||
|
||||
TME_DATAMATRIX_CODE = "PWBP-302 1PMPNWBP-302 Q1 K19361337/1"
|
||||
|
@ -8,7 +8,6 @@ import re
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.base.barcodes.mixins import SupplierBarcodeData
|
||||
from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
|
||||
|
||||
|
||||
@ -30,42 +29,45 @@ class TMEPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
||||
}
|
||||
}
|
||||
|
||||
def parse_supplier_barcode_data(self, barcode_data):
|
||||
TME_IS_QRCODE_REGEX = re.compile(r"([^\s:]+:[^\s:]+\s+)+(\S+(\s|$)+)+")
|
||||
TME_IS_BARCODE2D_REGEX = re.compile(r"(([^\s]+)(\s+|$))+")
|
||||
|
||||
# Custom field mapping
|
||||
TME_QRCODE_FIELDS = {
|
||||
"PN": SupplierBarcodeMixin.SUPPLIER_PART_NUMBER,
|
||||
"PO": SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER,
|
||||
"MPN": SupplierBarcodeMixin.MANUFACTURER_PART_NUMBER,
|
||||
"QTY": SupplierBarcodeMixin.QUANTITY,
|
||||
}
|
||||
|
||||
def extract_barcode_fields(self, barcode_data: str) -> dict[str, str]:
|
||||
"""Get supplier_part and barcode_fields from TME QR-Code or DataMatrix-Code."""
|
||||
|
||||
if not isinstance(barcode_data, str):
|
||||
return None
|
||||
|
||||
if TME_IS_QRCODE_REGEX.fullmatch(barcode_data):
|
||||
barcode_fields = {
|
||||
QRCODE_FIELD_NAME_MAP.get(field_name, field_name): value
|
||||
for field_name, value in TME_PARSE_QRCODE_REGEX.findall(barcode_data)
|
||||
}
|
||||
elif TME_IS_BARCODE2D_REGEX.fullmatch(barcode_data):
|
||||
barcode_fields = self.parse_ecia_barcode2d(
|
||||
TME_PARSE_BARCODE2D_REGEX.findall(barcode_data)
|
||||
)
|
||||
barcode_fields = {}
|
||||
|
||||
if self.TME_IS_QRCODE_REGEX.fullmatch(barcode_data):
|
||||
# Custom QR Code format e.g. "QTY: 1 PN:12345"
|
||||
for item in barcode_data.split(" "):
|
||||
if ":" in item:
|
||||
key, value = item.split(":")
|
||||
if key in self.TME_QRCODE_FIELDS:
|
||||
barcode_fields[self.TME_QRCODE_FIELDS[key]] = value
|
||||
|
||||
return barcode_fields
|
||||
|
||||
elif self.TME_IS_BARCODE2D_REGEX.fullmatch(barcode_data):
|
||||
# 2D Barcode format e.g. "PWBP-302 1PMPNWBP-302 Q1 K19361337/1"
|
||||
for item in barcode_data.split(" "):
|
||||
for k, v in self.ecia_field_map().items():
|
||||
if item.startswith(k):
|
||||
barcode_fields[v] = item[len(k):]
|
||||
else:
|
||||
return None
|
||||
return {}
|
||||
|
||||
if order_number := barcode_fields.get("purchase_order_number"):
|
||||
# Custom handling for order number
|
||||
if SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER in barcode_fields:
|
||||
order_number = barcode_fields[SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER]
|
||||
order_number = order_number.split("/")[0]
|
||||
barcode_fields[SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER] = order_number
|
||||
|
||||
return SupplierBarcodeData(
|
||||
SKU=barcode_fields.get("supplier_part_number"),
|
||||
MPN=barcode_fields.get("manufacturer_part_number"),
|
||||
quantity=barcode_fields.get("quantity"),
|
||||
order_number=order_number,
|
||||
)
|
||||
|
||||
|
||||
TME_IS_QRCODE_REGEX = re.compile(r"([^\s:]+:[^\s:]+\s+)+(\S+(\s|$)+)+")
|
||||
TME_PARSE_QRCODE_REGEX = re.compile(r"([^\s:]+):([^\s:]+)(?:\s+|$)")
|
||||
TME_IS_BARCODE2D_REGEX = re.compile(r"(([^\s]+)(\s+|$))+")
|
||||
TME_PARSE_BARCODE2D_REGEX = re.compile(r"([^\s]+)(?:\s+|$)")
|
||||
QRCODE_FIELD_NAME_MAP = {
|
||||
"PN": "supplier_part_number",
|
||||
"PO": "purchase_order_number",
|
||||
"MPN": "manufacturer_part_number",
|
||||
"QTY": "quantity",
|
||||
}
|
||||
return barcode_fields
|
||||
|
Reference in New Issue
Block a user