mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +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:
parent
538a01c500
commit
df0da18d2f
@ -340,7 +340,7 @@ class BarcodePOReceive(APIView):
|
|||||||
|
|
||||||
# A plugin has not been found!
|
# A plugin has not been found!
|
||||||
if plugin is None:
|
if plugin is None:
|
||||||
response["error"] = _("Invalid supplier barcode")
|
response["error"] = _("No match for supplier barcode")
|
||||||
raise ValidationError(response)
|
raise ValidationError(response)
|
||||||
elif "error" in response:
|
elif "error" in response:
|
||||||
raise ValidationError(response)
|
raise ValidationError(response)
|
||||||
@ -352,7 +352,7 @@ barcode_api_urls = [
|
|||||||
# Link a third-party barcode to an item (e.g. Part / StockItem / etc)
|
# Link a third-party barcode to an item (e.g. Part / StockItem / etc)
|
||||||
path('link/', BarcodeAssign.as_view(), name='api-barcode-link'),
|
path('link/', BarcodeAssign.as_view(), name='api-barcode-link'),
|
||||||
|
|
||||||
# Unlink a third-pary barcode from an item
|
# Unlink a third-party barcode from an item
|
||||||
path('unlink/', BarcodeUnassign.as_view(), name='api-barcode-unlink'),
|
path('unlink/', BarcodeUnassign.as_view(), name='api-barcode-unlink'),
|
||||||
|
|
||||||
# Receive a purchase order item by scanning its barcode
|
# Receive a purchase order item by scanning its barcode
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -55,52 +54,99 @@ class BarcodeMixin:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SupplierBarcodeData:
|
|
||||||
"""Data parsed from a supplier barcode."""
|
|
||||||
SKU: str = None
|
|
||||||
MPN: str = None
|
|
||||||
quantity: Decimal | str = None
|
|
||||||
order_number: str = None
|
|
||||||
|
|
||||||
|
|
||||||
class SupplierBarcodeMixin(BarcodeMixin):
|
class SupplierBarcodeMixin(BarcodeMixin):
|
||||||
"""Mixin that provides default implementations for scan functions for supplier barcodes.
|
"""Mixin that provides default implementations for scan functions for supplier barcodes.
|
||||||
|
|
||||||
Custom supplier barcode plugins should use this mixin and implement the
|
Custom supplier barcode plugins should use this mixin and implement the
|
||||||
parse_supplier_barcode_data function.
|
extract_barcode_fields function.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Set of standard field names which can be extracted from the barcode
|
||||||
|
CUSTOMER_ORDER_NUMBER = "customer_order_number"
|
||||||
|
SUPPLIER_ORDER_NUMBER = "supplier_order_number"
|
||||||
|
PACKING_LIST_NUMBER = "packing_list_number"
|
||||||
|
SHIP_DATE = "ship_date"
|
||||||
|
CUSTOMER_PART_NUMBER = "customer_part_number"
|
||||||
|
SUPPLIER_PART_NUMBER = "supplier_part_number"
|
||||||
|
PURCHASE_ORDER_LINE = "purchase_order_line"
|
||||||
|
QUANTITY = "quantity"
|
||||||
|
DATE_CODE = "date_code"
|
||||||
|
LOT_CODE = "lot_code"
|
||||||
|
COUNTRY_OF_ORIGIN = "country_of_origin"
|
||||||
|
MANUFACTURER = "manufacturer"
|
||||||
|
MANUFACTURER_PART_NUMBER = "manufacturer_part_number"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Register mixin."""
|
"""Register mixin."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.add_mixin('supplier-barcode', True, __class__)
|
self.add_mixin('supplier-barcode', True, __class__)
|
||||||
|
|
||||||
def parse_supplier_barcode_data(self, barcode_data) -> SupplierBarcodeData | None:
|
def get_field_value(self, key, backup_value=None):
|
||||||
"""Get supplier_part and other barcode_fields from barcode data.
|
"""Return the value of a barcode field."""
|
||||||
|
fields = getattr(self, "barcode_fields", None) or {}
|
||||||
|
|
||||||
|
return fields.get(key, backup_value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def quantity(self):
|
||||||
|
"""Return the quantity from the barcode fields."""
|
||||||
|
return self.get_field_value(self.QUANTITY)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supplier_part_number(self):
|
||||||
|
"""Return the supplier part number from the barcode fields."""
|
||||||
|
return self.get_field_value(self.SUPPLIER_PART_NUMBER)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manufacturer_part_number(self):
|
||||||
|
"""Return the manufacturer part number from the barcode fields."""
|
||||||
|
return self.get_field_value(self.MANUFACTURER_PART_NUMBER)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def customer_order_number(self):
|
||||||
|
"""Return the customer order number from the barcode fields."""
|
||||||
|
return self.get_field_value(self.CUSTOMER_ORDER_NUMBER)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supplier_order_number(self):
|
||||||
|
"""Return the supplier order number from the barcode fields."""
|
||||||
|
return self.get_field_value(self.SUPPLIER_ORDER_NUMBER)
|
||||||
|
|
||||||
|
def extract_barcode_fields(self, barcode_data) -> dict[str, str]:
|
||||||
|
"""Method to extract barcode fields from barcode data.
|
||||||
|
|
||||||
|
This method should return a dict object where the keys are the field names,
|
||||||
|
as per the "standard field names" (defined in the SuppliedBarcodeMixin class).
|
||||||
|
|
||||||
|
This method *must* be implemented by each plugin
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None if the barcode_data is not from a valid barcode of the supplier.
|
A dict object containing the barcode fields.
|
||||||
|
|
||||||
A SupplierBarcodeData object containing the SKU, MPN, quantity and order number
|
|
||||||
if available.
|
|
||||||
"""
|
"""
|
||||||
|
raise NotImplementedError("extract_barcode_fields must be implemented by each plugin")
|
||||||
return None
|
|
||||||
|
|
||||||
def scan(self, barcode_data):
|
def scan(self, barcode_data):
|
||||||
"""Try to match a supplier barcode to a supplier part."""
|
"""Try to match a supplier barcode to a supplier part."""
|
||||||
|
|
||||||
if not (parsed := self.parse_supplier_barcode_data(barcode_data)):
|
barcode_data = str(barcode_data).strip()
|
||||||
return None
|
|
||||||
if parsed.SKU is None and parsed.MPN is None:
|
self.barcode_fields = self.extract_barcode_fields(barcode_data)
|
||||||
|
|
||||||
|
if self.supplier_part_number is None and self.manufacturer_part_number is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
supplier_parts = self.get_supplier_parts(parsed.SKU, self.get_supplier(), parsed.MPN)
|
supplier_parts = self.get_supplier_parts(
|
||||||
|
sku=self.supplier_part_number,
|
||||||
|
mpn=self.manufacturer_part_number,
|
||||||
|
supplier=self.get_supplier(),
|
||||||
|
)
|
||||||
|
|
||||||
if len(supplier_parts) > 1:
|
if len(supplier_parts) > 1:
|
||||||
return {"error": _("Found multiple matching supplier parts for barcode")}
|
return {"error": _("Found multiple matching supplier parts for barcode")}
|
||||||
elif not supplier_parts:
|
elif not supplier_parts:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
supplier_part = supplier_parts[0]
|
supplier_part = supplier_parts[0]
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@ -109,28 +155,61 @@ class SupplierBarcodeMixin(BarcodeMixin):
|
|||||||
"web_url": supplier_part.get_absolute_url(),
|
"web_url": supplier_part.get_absolute_url(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {SupplierPart.barcode_model_type(): data}
|
return {
|
||||||
|
SupplierPart.barcode_model_type(): data
|
||||||
|
}
|
||||||
|
|
||||||
def scan_receive_item(self, barcode_data, user, purchase_order=None, location=None):
|
def scan_receive_item(self, barcode_data, user, purchase_order=None, location=None):
|
||||||
"""Try to scan a supplier barcode to receive a purchase order item."""
|
"""Try to scan a supplier barcode to receive a purchase order item."""
|
||||||
|
|
||||||
if not (parsed := self.parse_supplier_barcode_data(barcode_data)):
|
barcode_data = str(barcode_data).strip()
|
||||||
return None
|
|
||||||
if parsed.SKU is None and parsed.MPN is None:
|
self.barcode_fields = self.extract_barcode_fields(barcode_data)
|
||||||
|
|
||||||
|
if self.supplier_part_number is None and self.manufacturer_part_number is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
supplier_parts = self.get_supplier_parts(parsed.SKU, self.get_supplier(), parsed.MPN)
|
supplier = self.get_supplier()
|
||||||
|
|
||||||
|
supplier_parts = self.get_supplier_parts(
|
||||||
|
sku=self.supplier_part_number,
|
||||||
|
mpn=self.manufacturer_part_number,
|
||||||
|
supplier=supplier,
|
||||||
|
)
|
||||||
|
|
||||||
if len(supplier_parts) > 1:
|
if len(supplier_parts) > 1:
|
||||||
return {"error": _("Found multiple matching supplier parts for barcode")}
|
return {"error": _("Found multiple matching supplier parts for barcode")}
|
||||||
elif not supplier_parts:
|
elif not supplier_parts:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
supplier_part = supplier_parts[0]
|
supplier_part = supplier_parts[0]
|
||||||
|
|
||||||
|
# If a purchase order is not provided, extract it from the provided data
|
||||||
|
if not purchase_order:
|
||||||
|
matching_orders = self.get_purchase_orders(
|
||||||
|
self.customer_order_number,
|
||||||
|
self.supplier_order_number,
|
||||||
|
supplier=supplier,
|
||||||
|
)
|
||||||
|
|
||||||
|
order = self.customer_order_number or self.supplier_order_number
|
||||||
|
|
||||||
|
if len(matching_orders) > 1:
|
||||||
|
return {"error": _(f"Found multiple purchase orders matching '{order}'")}
|
||||||
|
|
||||||
|
if len(matching_orders) == 0:
|
||||||
|
return {"error": _(f"No matching purchase order for '{order}'")}
|
||||||
|
|
||||||
|
purchase_order = matching_orders.first()
|
||||||
|
|
||||||
|
if supplier and purchase_order:
|
||||||
|
if purchase_order.supplier != supplier:
|
||||||
|
return {"error": _("Purchase order does not match supplier")}
|
||||||
|
|
||||||
return self.receive_purchase_order_item(
|
return self.receive_purchase_order_item(
|
||||||
supplier_part,
|
supplier_part,
|
||||||
user,
|
user,
|
||||||
quantity=parsed.quantity,
|
quantity=self.quantity,
|
||||||
order_number=parsed.order_number,
|
|
||||||
purchase_order=purchase_order,
|
purchase_order=purchase_order,
|
||||||
location=location,
|
location=location,
|
||||||
barcode=barcode_data,
|
barcode=barcode_data,
|
||||||
@ -159,52 +238,124 @@ class SupplierBarcodeMixin(BarcodeMixin):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
suppliers = Company.objects.filter(name__icontains=supplier_name, is_supplier=True)
|
suppliers = Company.objects.filter(name__icontains=supplier_name, is_supplier=True)
|
||||||
|
|
||||||
if len(suppliers) != 1:
|
if len(suppliers) != 1:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self.set_setting("SUPPLIER_ID", suppliers.first().pk)
|
self.set_setting("SUPPLIER_ID", suppliers.first().pk)
|
||||||
|
|
||||||
return suppliers.first()
|
return suppliers.first()
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def parse_ecia_barcode2d(barcode_data: str | list[str]) -> dict[str, str]:
|
def ecia_field_map(cls):
|
||||||
"""Parse a standard ECIA 2D barcode, according to https://www.ecianow.org/assets/docs/ECIA_Specifications.pdf"""
|
"""Return a dict mapping ECIA field names to internal field names
|
||||||
|
|
||||||
if not isinstance(barcode_data, str):
|
Ref: https://www.ecianow.org/assets/docs/ECIA_Specifications.pdf
|
||||||
data_split = barcode_data
|
|
||||||
elif not (data_split := SupplierBarcodeMixin.parse_isoiec_15434_barcode2d(barcode_data)):
|
Note that a particular plugin may need to reimplement this method,
|
||||||
return None
|
if it does not use the standard field names.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"K": cls.CUSTOMER_ORDER_NUMBER,
|
||||||
|
"1K": cls.SUPPLIER_ORDER_NUMBER,
|
||||||
|
"11K": cls.PACKING_LIST_NUMBER,
|
||||||
|
"6D": cls.SHIP_DATE,
|
||||||
|
"9D": cls.DATE_CODE,
|
||||||
|
"10D": cls.DATE_CODE,
|
||||||
|
"4K": cls.PURCHASE_ORDER_LINE,
|
||||||
|
"14K": cls.PURCHASE_ORDER_LINE,
|
||||||
|
"P": cls.SUPPLIER_PART_NUMBER,
|
||||||
|
"1P": cls.MANUFACTURER_PART_NUMBER,
|
||||||
|
"30P": cls.SUPPLIER_PART_NUMBER,
|
||||||
|
"1T": cls.LOT_CODE,
|
||||||
|
"4L": cls.COUNTRY_OF_ORIGIN,
|
||||||
|
"1V": cls.MANUFACTURER,
|
||||||
|
"Q": cls.QUANTITY,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_ecia_barcode2d(cls, barcode_data: str) -> dict[str, str]:
|
||||||
|
"""Parse a standard ECIA 2D barcode
|
||||||
|
|
||||||
|
Ref: https://www.ecianow.org/assets/docs/ECIA_Specifications.pdf
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
barcode_data: The raw barcode data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict containing the parsed barcode fields
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Split data into separate fields
|
||||||
|
fields = cls.parse_isoiec_15434_barcode2d(barcode_data)
|
||||||
|
|
||||||
barcode_fields = {}
|
barcode_fields = {}
|
||||||
for entry in data_split:
|
|
||||||
for identifier, field_name in ECIA_DATA_IDENTIFIER_MAP.items():
|
if not fields:
|
||||||
if entry.startswith(identifier):
|
return barcode_fields
|
||||||
barcode_fields[field_name] = entry[len(identifier):]
|
|
||||||
|
for field in fields:
|
||||||
|
for identifier, field_name in cls.ecia_field_map().items():
|
||||||
|
if field.startswith(identifier):
|
||||||
|
barcode_fields[field_name] = field[len(identifier):]
|
||||||
break
|
break
|
||||||
|
|
||||||
return barcode_fields
|
return barcode_fields
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def split_fields(barcode_data: str, delimiter: str = ',', header: str = '', trailer: str = '') -> list[str]:
|
||||||
|
"""Generic method for splitting barcode data into separate fields"""
|
||||||
|
|
||||||
|
if header and barcode_data.startswith(header):
|
||||||
|
barcode_data = barcode_data[len(header):]
|
||||||
|
|
||||||
|
if trailer and barcode_data.endswith(trailer):
|
||||||
|
barcode_data = barcode_data[:-len(trailer)]
|
||||||
|
|
||||||
|
return barcode_data.split(delimiter)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_isoiec_15434_barcode2d(barcode_data: str) -> list[str]:
|
def parse_isoiec_15434_barcode2d(barcode_data: str) -> list[str]:
|
||||||
"""Parse a ISO/IEC 15434 bardode, returning the split data section."""
|
"""Parse a ISO/IEC 15434 barcode, returning the split data section."""
|
||||||
|
|
||||||
|
OLD_MOUSER_HEADER = ">[)>06\x1D"
|
||||||
HEADER = "[)>\x1E06\x1D"
|
HEADER = "[)>\x1E06\x1D"
|
||||||
TRAILER = "\x1E\x04"
|
TRAILER = "\x1E\x04"
|
||||||
|
DELIMITER = "\x1D"
|
||||||
|
|
||||||
# some old mouser barcodes start with this messed up header
|
# Some old mouser barcodes start with this messed up header
|
||||||
OLD_MOUSER_HEADER = ">[)>06\x1D"
|
|
||||||
if barcode_data.startswith(OLD_MOUSER_HEADER):
|
if barcode_data.startswith(OLD_MOUSER_HEADER):
|
||||||
barcode_data = barcode_data.replace(OLD_MOUSER_HEADER, HEADER, 1)
|
barcode_data = barcode_data.replace(OLD_MOUSER_HEADER, HEADER, 1)
|
||||||
|
|
||||||
# most barcodes don't include the trailer, because "why would you stick to
|
# Check that the barcode starts with the necessary header
|
||||||
# the standard, right?" so we only check for the header here
|
|
||||||
if not barcode_data.startswith(HEADER):
|
if not barcode_data.startswith(HEADER):
|
||||||
return
|
return
|
||||||
|
|
||||||
actual_data = barcode_data.split(HEADER, 1)[1].rsplit(TRAILER, 1)[0]
|
return SupplierBarcodeMixin.split_fields(
|
||||||
|
barcode_data,
|
||||||
return actual_data.split("\x1D")
|
delimiter=DELIMITER,
|
||||||
|
header=HEADER,
|
||||||
|
trailer=TRAILER,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_supplier_parts(sku: str, supplier: Company = None, mpn: str = None):
|
def get_purchase_orders(customer_order_number, supplier_order_number, supplier: Company = None):
|
||||||
|
"""Attempt to find a purchase order from the extracted customer and supplier order numbers"""
|
||||||
|
|
||||||
|
orders = PurchaseOrder.objects.filter(status=PurchaseOrderStatus.PLACED.value)
|
||||||
|
|
||||||
|
if supplier:
|
||||||
|
orders = orders.filter(supplier=supplier)
|
||||||
|
|
||||||
|
if customer_order_number:
|
||||||
|
orders = orders.filter(reference__iexact=customer_order_number)
|
||||||
|
elif supplier_order_number:
|
||||||
|
orders = orders.filter(supplier_reference__iexact=supplier_order_number)
|
||||||
|
|
||||||
|
return orders
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_supplier_parts(sku: str = None, supplier: Company = None, mpn: str = None):
|
||||||
"""Get a supplier part from SKU or by supplier and MPN."""
|
"""Get a supplier part from SKU or by supplier and MPN."""
|
||||||
if not (sku or supplier or mpn):
|
if not (sku or supplier or mpn):
|
||||||
return SupplierPart.objects.none()
|
return SupplierPart.objects.none()
|
||||||
@ -241,7 +392,6 @@ class SupplierBarcodeMixin(BarcodeMixin):
|
|||||||
supplier_part: SupplierPart,
|
supplier_part: SupplierPart,
|
||||||
user: User,
|
user: User,
|
||||||
quantity: Decimal | str = None,
|
quantity: Decimal | str = None,
|
||||||
order_number: str = None,
|
|
||||||
purchase_order: PurchaseOrder = None,
|
purchase_order: PurchaseOrder = None,
|
||||||
location: StockLocation = None,
|
location: StockLocation = None,
|
||||||
barcode: str = None,
|
barcode: str = None,
|
||||||
@ -255,27 +405,6 @@ class SupplierBarcodeMixin(BarcodeMixin):
|
|||||||
- on failure: an "error" message
|
- on failure: an "error" message
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not purchase_order:
|
|
||||||
# try to find a purchase order with either reference or name matching
|
|
||||||
# the provided order_number
|
|
||||||
if not order_number:
|
|
||||||
return {"error": _("Supplier barcode doesn't contain order number")}
|
|
||||||
|
|
||||||
purchase_orders = (
|
|
||||||
PurchaseOrder.objects.filter(
|
|
||||||
supplier_reference__iexact=order_number,
|
|
||||||
status=PurchaseOrderStatus.PLACED.value,
|
|
||||||
) | PurchaseOrder.objects.filter(
|
|
||||||
reference__iexact=order_number,
|
|
||||||
status=PurchaseOrderStatus.PLACED.value,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(purchase_orders) > 1:
|
|
||||||
return {"error": _(f"Found multiple placed purchase orders for '{order_number}'")}
|
|
||||||
elif not (purchase_order := purchase_orders.first()):
|
|
||||||
return {"error": _(f"Failed to find placed purchase order for '{order_number}'")}
|
|
||||||
|
|
||||||
if quantity:
|
if quantity:
|
||||||
try:
|
try:
|
||||||
quantity = Decimal(quantity)
|
quantity = Decimal(quantity)
|
||||||
@ -350,23 +479,3 @@ class SupplierBarcodeMixin(BarcodeMixin):
|
|||||||
|
|
||||||
response["success"] = _("Received purchase order line item")
|
response["success"] = _("Received purchase order line item")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
# Map ECIA Data Identifier to human readable identifier
|
|
||||||
# The following identifiers haven't been implemented: 3S, 4S, 5S, S
|
|
||||||
ECIA_DATA_IDENTIFIER_MAP = {
|
|
||||||
"K": "purchase_order_number", # noqa: E241
|
|
||||||
"1K": "purchase_order_number", # noqa: E241 DigiKey uses 1K instead of K
|
|
||||||
"11K": "packing_list_number", # noqa: E241
|
|
||||||
"6D": "ship_date", # noqa: E241
|
|
||||||
"P": "supplier_part_number", # noqa: E241 "Customer Part Number"
|
|
||||||
"1P": "manufacturer_part_number", # noqa: E241 "Supplier Part Number"
|
|
||||||
"4K": "purchase_order_line", # noqa: E241
|
|
||||||
"14K": "purchase_order_line", # noqa: E241 Mouser uses 14K instead of 4K
|
|
||||||
"Q": "quantity", # noqa: E241
|
|
||||||
"9D": "date_yyww", # noqa: E241
|
|
||||||
"10D": "date_yyww", # noqa: E241
|
|
||||||
"1T": "lot_code", # noqa: E241
|
|
||||||
"4L": "country_of_origin", # noqa: E241
|
|
||||||
"1V": "manufacturer" # noqa: E241
|
|
||||||
}
|
|
||||||
|
@ -6,7 +6,6 @@ This plugin can currently only match DigiKey barcodes to supplier parts.
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from plugin import InvenTreePlugin
|
from plugin import InvenTreePlugin
|
||||||
from plugin.base.barcodes.mixins import SupplierBarcodeData
|
|
||||||
from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
|
from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +19,7 @@ class DigiKeyPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
|||||||
AUTHOR = _("InvenTree contributors")
|
AUTHOR = _("InvenTree contributors")
|
||||||
|
|
||||||
DEFAULT_SUPPLIER_NAME = "DigiKey"
|
DEFAULT_SUPPLIER_NAME = "DigiKey"
|
||||||
|
|
||||||
SETTINGS = {
|
SETTINGS = {
|
||||||
"SUPPLIER_ID": {
|
"SUPPLIER_ID": {
|
||||||
"name": _("Supplier"),
|
"name": _("Supplier"),
|
||||||
@ -28,22 +28,7 @@ class DigiKeyPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_supplier_barcode_data(self, barcode_data):
|
def extract_barcode_fields(self, barcode_data) -> dict[str, str]:
|
||||||
"""Get supplier_part and barcode_fields from DigiKey DataMatrix-Code."""
|
"""Extract barcode fields from a DigiKey plugin"""
|
||||||
|
|
||||||
if not isinstance(barcode_data, str):
|
return self.parse_ecia_barcode2d(barcode_data)
|
||||||
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
|
|
||||||
|
|
||||||
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"),
|
|
||||||
)
|
|
||||||
|
@ -8,7 +8,6 @@ import re
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from plugin import InvenTreePlugin
|
from plugin import InvenTreePlugin
|
||||||
from plugin.base.barcodes.mixins import SupplierBarcodeData
|
|
||||||
from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
|
from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
|
||||||
|
|
||||||
|
|
||||||
@ -30,23 +29,40 @@ class LCSCPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_supplier_barcode_data(self, barcode_data):
|
LCSC_BARCODE_REGEX = re.compile(r"^{((?:[^:,]+:[^:,]*,)*(?:[^:,]+:[^:,]*))}$")
|
||||||
"""Get supplier_part and barcode_fields from LCSC QR-Code."""
|
|
||||||
|
|
||||||
if not isinstance(barcode_data, str):
|
# Custom field mapping for LCSC barcodes
|
||||||
return None
|
LCSC_FIELDS = {
|
||||||
|
"pm": SupplierBarcodeMixin.MANUFACTURER_PART_NUMBER,
|
||||||
|
"pc": SupplierBarcodeMixin.SUPPLIER_PART_NUMBER,
|
||||||
|
"qty": SupplierBarcodeMixin.QUANTITY,
|
||||||
|
"on": SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER,
|
||||||
|
}
|
||||||
|
|
||||||
if not (match := LCSC_BARCODE_REGEX.fullmatch(barcode_data)):
|
def extract_barcode_fields(self, barcode_data: str) -> dict[str, str]:
|
||||||
return None
|
"""Get supplier_part and barcode_fields from LCSC QR-Code.
|
||||||
|
|
||||||
barcode_fields = dict(pair.split(":") for pair in match.group(1).split(","))
|
Example LCSC QR-Code: {pbn:PICK2009291337,on:SO2009291337,pc:C312270}
|
||||||
|
"""
|
||||||
|
|
||||||
return SupplierBarcodeData(
|
if not self.LCSC_BARCODE_REGEX.fullmatch(barcode_data):
|
||||||
SKU=barcode_fields.get("pc"),
|
return {}
|
||||||
MPN=barcode_fields.get("pm"),
|
|
||||||
quantity=barcode_fields.get("qty"),
|
# Extract fields
|
||||||
order_number=barcode_fields.get("on"),
|
fields = SupplierBarcodeMixin.split_fields(
|
||||||
|
barcode_data,
|
||||||
|
delimiter=',',
|
||||||
|
header='{',
|
||||||
|
trailer='}',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fields = dict(pair.split(":") for pair in fields)
|
||||||
|
|
||||||
LCSC_BARCODE_REGEX = re.compile(r"^{((?:[^:,]+:[^:,]*,)*(?:[^:,]+:[^:,]*))}$")
|
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]
|
||||||
|
|
||||||
|
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 django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from plugin import InvenTreePlugin
|
from plugin import InvenTreePlugin
|
||||||
from plugin.base.barcodes.mixins import SupplierBarcodeData
|
|
||||||
from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
|
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."""
|
"""Get supplier_part and barcode_fields from Mouser DataMatrix-Code."""
|
||||||
|
|
||||||
if not isinstance(barcode_data, str):
|
return self.parse_ecia_barcode2d(barcode_data)
|
||||||
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"),
|
|
||||||
)
|
|
||||||
|
@ -12,6 +12,8 @@ from stock.models import StockItem, StockLocation
|
|||||||
class SupplierBarcodeTests(InvenTreeAPITestCase):
|
class SupplierBarcodeTests(InvenTreeAPITestCase):
|
||||||
"""Tests barcode parsing for all suppliers."""
|
"""Tests barcode parsing for all suppliers."""
|
||||||
|
|
||||||
|
SCAN_URL = reverse("api-barcode-scan")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
"""Create supplier parts for barcodes."""
|
"""Create supplier parts for barcodes."""
|
||||||
@ -41,76 +43,90 @@ class SupplierBarcodeTests(InvenTreeAPITestCase):
|
|||||||
SupplierPart.objects.bulk_create(supplier_parts)
|
SupplierPart.objects.bulk_create(supplier_parts)
|
||||||
|
|
||||||
def test_digikey_barcode(self):
|
def test_digikey_barcode(self):
|
||||||
"""Test digikey barcode."""
|
"""Test digikey barcode"""
|
||||||
|
|
||||||
url = reverse("api-barcode-scan")
|
result = self.post(self.SCAN_URL, data={"barcode": DIGIKEY_BARCODE}, expected_code=200)
|
||||||
result = self.post(url, data={"barcode": DIGIKEY_BARCODE})
|
self.assertEqual(result.data['plugin'], 'DigiKeyPlugin')
|
||||||
|
|
||||||
supplier_part_data = result.data.get("supplierpart")
|
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"])
|
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_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"])
|
||||||
|
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):
|
def test_mouser_barcode(self):
|
||||||
"""Test mouser barcode with custom order number."""
|
"""Test mouser barcode with custom order number."""
|
||||||
|
|
||||||
url = reverse("api-barcode-scan")
|
result = self.post(self.SCAN_URL, data={"barcode": MOUSER_BARCODE}, expected_code=200)
|
||||||
result = self.post(url, data={"barcode": MOUSER_BARCODE})
|
|
||||||
|
|
||||||
supplier_part_data = result.data.get("supplierpart")
|
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"])
|
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):
|
def test_old_mouser_barcode(self):
|
||||||
"""Test old mouser barcode with messed up header."""
|
"""Test old mouser barcode with messed up header."""
|
||||||
|
|
||||||
url = reverse("api-barcode-scan")
|
result = self.post(self.SCAN_URL, data={"barcode": MOUSER_BARCODE_OLD}, expected_code=200)
|
||||||
result = self.post(url, data={"barcode": MOUSER_BARCODE_OLD})
|
|
||||||
|
|
||||||
supplier_part_data = result.data.get("supplierpart")
|
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"])
|
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):
|
def test_lcsc_barcode(self):
|
||||||
"""Test LCSC barcode."""
|
"""Test LCSC barcode."""
|
||||||
|
|
||||||
url = reverse("api-barcode-scan")
|
result = self.post(self.SCAN_URL, data={"barcode": LCSC_BARCODE}, expected_code=200)
|
||||||
result = self.post(url, data={"barcode": LCSC_BARCODE})
|
|
||||||
|
self.assertEqual(result.data['plugin'], 'LCSCPlugin')
|
||||||
|
|
||||||
supplier_part_data = result.data.get("supplierpart")
|
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"])
|
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):
|
def test_tme_qrcode(self):
|
||||||
"""Test TME QR-Code."""
|
"""Test TME QR-Code."""
|
||||||
|
|
||||||
url = reverse("api-barcode-scan")
|
result = self.post(self.SCAN_URL, data={"barcode": TME_QRCODE}, expected_code=200)
|
||||||
result = self.post(url, data={"barcode": TME_QRCODE})
|
|
||||||
|
self.assertEqual(result.data['plugin'], 'TMEPlugin')
|
||||||
|
|
||||||
supplier_part_data = result.data.get("supplierpart")
|
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"])
|
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):
|
def test_tme_barcode2d(self):
|
||||||
"""Test TME DataMatrix-Code."""
|
"""Test TME DataMatrix-Code."""
|
||||||
|
|
||||||
url = reverse("api-barcode-scan")
|
result = self.post(self.SCAN_URL, data={"barcode": TME_DATAMATRIX_CODE}, expected_code=200)
|
||||||
result = self.post(url, data={"barcode": TME_DATAMATRIX_CODE})
|
|
||||||
|
self.assertEqual(result.data['plugin'], 'TMEPlugin')
|
||||||
|
|
||||||
supplier_part_data = result.data.get("supplierpart")
|
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"])
|
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):
|
class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||||
@ -161,7 +177,7 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
result1 = self.post(url, data={"barcode": DIGIKEY_BARCODE})
|
result1 = self.post(url, data={"barcode": DIGIKEY_BARCODE})
|
||||||
assert result1.status_code == 400
|
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()
|
self.purchase_order1.place_order()
|
||||||
|
|
||||||
@ -273,9 +289,10 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
url = reverse("api-barcode-po-receive")
|
url = reverse("api-barcode-po-receive")
|
||||||
barcode = MOUSER_BARCODE.replace("\x1dQ3", "")
|
barcode = MOUSER_BARCODE.replace("\x1dQ3", "")
|
||||||
result = self.post(url, data={"barcode": barcode})
|
response = self.post(url, data={"barcode": barcode}, expected_code=200)
|
||||||
assert "lineitem" in result.data
|
|
||||||
assert "quantity" not in result.data["lineitem"]
|
assert "lineitem" in response.data
|
||||||
|
assert "quantity" not in response.data["lineitem"]
|
||||||
|
|
||||||
|
|
||||||
DIGIKEY_BARCODE = (
|
DIGIKEY_BARCODE = (
|
||||||
@ -286,6 +303,25 @@ DIGIKEY_BARCODE = (
|
|||||||
"0000000000000000000000000000000000"
|
"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 = (
|
MOUSER_BARCODE = (
|
||||||
"[)>\x1e06\x1dKP0-1337\x1d14K011\x1d1PMC34063ADR\x1dQ3\x1d11K073121337\x1d4"
|
"[)>\x1e06\x1dKP0-1337\x1d14K011\x1d1PMC34063ADR\x1dQ3\x1d11K073121337\x1d4"
|
||||||
"LMX\x1d1VTI\x1e\x04"
|
"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"
|
"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"
|
"SE MPN:WBP-302 RoHS https://www.tme.eu/details/WBP-302"
|
||||||
)
|
)
|
||||||
|
|
||||||
TME_DATAMATRIX_CODE = "PWBP-302 1PMPNWBP-302 Q1 K19361337/1"
|
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 django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from plugin import InvenTreePlugin
|
from plugin import InvenTreePlugin
|
||||||
from plugin.base.barcodes.mixins import SupplierBarcodeData
|
|
||||||
from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
|
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."""
|
"""Get supplier_part and barcode_fields from TME QR-Code or DataMatrix-Code."""
|
||||||
|
|
||||||
if not isinstance(barcode_data, str):
|
barcode_fields = {}
|
||||||
return None
|
|
||||||
|
|
||||||
if TME_IS_QRCODE_REGEX.fullmatch(barcode_data):
|
if self.TME_IS_QRCODE_REGEX.fullmatch(barcode_data):
|
||||||
barcode_fields = {
|
# Custom QR Code format e.g. "QTY: 1 PN:12345"
|
||||||
QRCODE_FIELD_NAME_MAP.get(field_name, field_name): value
|
for item in barcode_data.split(" "):
|
||||||
for field_name, value in TME_PARSE_QRCODE_REGEX.findall(barcode_data)
|
if ":" in item:
|
||||||
}
|
key, value = item.split(":")
|
||||||
elif TME_IS_BARCODE2D_REGEX.fullmatch(barcode_data):
|
if key in self.TME_QRCODE_FIELDS:
|
||||||
barcode_fields = self.parse_ecia_barcode2d(
|
barcode_fields[self.TME_QRCODE_FIELDS[key]] = value
|
||||||
TME_PARSE_BARCODE2D_REGEX.findall(barcode_data)
|
|
||||||
)
|
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:
|
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]
|
order_number = order_number.split("/")[0]
|
||||||
|
barcode_fields[SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER] = order_number
|
||||||
|
|
||||||
return SupplierBarcodeData(
|
return barcode_fields
|
||||||
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",
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user