mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Add basic support for scanning vendor barcodes (#5509)
* Add support for scanning digikey and mouser barcodes * added small fixes if no part is found * made small style cleanups * Separate out ecia 2d barcode parser, Add quantity, PO number to response * Use model instead of name for mouser supplier, add auto detection magic * Add lcsc barcode support * Move barcode plugins to new suppliers subdirectory * Add get_supplier_part helper, Refactor plugins in preparation for #3791 * Add __init__.py to suppliers directory * Improve formatting * Add barcode integration tests * Add api-barcode-po-receive endpoint * Refactor supplier_barcode.py helpers into BarcodeMixin * Implement the api-barcode-po-receive endpoint for all suppliers * Always include lineitem in api response * Fix location in response, only include quantity and location if set * Check if barcode has already been assigned, Fix tests * FIx quantity and location not being in lineitem reponse * Use part.get_default_location() instead of part.default_location * Fix fomatting again * Fix type annotations for python 3.8 * Add get_supplier_part helper, check for barcode_data being a str * Fix naming clash * Clarify return type for scan_receive_item * Improve model access using first() in two places * Refactor a bunch of checks * Improve selection of line item, if multiple line items match the SKU * Add new api version for this PR * Fix error if no line item exists * Add debug print to investigate why tests are failing * Remove the test print again * Fix pre formatted log messages * Test removing all plugins * Test only with digikey plugin * Test with all plugins, but without mouser "model" setting * Test again without tests * Test with simple tests * Test with simple receive test * Test with even more receive tests * Test second receive test * Test third receive test * Test 4th receive test with debug prints * Try deleting the stock item and stock locations * Disable the test again * Add SupplierBarcodeMixin to minimize shared code between plugins * Add TME supplier barcode plugin * Remove the TME tests again * If this works the tests are broken, if this doesn't work the tests are broken too * Add TME tests again * Add back all tests again * Fix TME purchase order number * Fix TME qrcode regex * Add documentation for this feature * Fix TME qrcode regex * Use Decimal instead of int for quantity * Refactor get_supplier_parts, Add get_supplier method * Improve docstrings * Fix None type access * FIx TME barcode detection, Improve supplier barcode handling * Try to retrigger pipeline * Refactor get_supplier_parts to not use lists * Add DEFAULT_SUPPLIER_NAME to mouser plugin * Add SUPPLIER_ID setting to other suppliers * Fix supplier plugins not inheriting from settings mixin --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
parent
2be5ec26f8
commit
ae063d2722
@ -2,11 +2,14 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 138
|
||||
INVENTREE_API_VERSION = 139
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v139 -> 2023-10-11 : https://github.com/inventree/InvenTree/pull/5509
|
||||
- Add new BarcodePOReceive endpoint to receive line items by scanning supplier barcodes
|
||||
|
||||
v138 -> 2023-10-11 : https://github.com/inventree/InvenTree/pull/5679
|
||||
- Settings keys are no longer case sensitive
|
||||
- Include settings units in API serializer
|
||||
|
@ -10,9 +10,11 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from InvenTree.helpers import hash_barcode
|
||||
from order.models import PurchaseOrder
|
||||
from plugin import registry
|
||||
from plugin.builtin.barcodes.inventree_barcode import \
|
||||
InvenTreeInternalBarcodePlugin
|
||||
from stock.models import StockLocation
|
||||
from users.models import RuleSet
|
||||
|
||||
|
||||
@ -230,7 +232,7 @@ class BarcodeUnassign(APIView):
|
||||
instance.unassign_barcode()
|
||||
|
||||
return Response({
|
||||
'success': 'Barcode unassigned from {label} instance',
|
||||
'success': f'Barcode unassigned from {label} instance',
|
||||
})
|
||||
|
||||
# If we get to this point, something has gone wrong!
|
||||
@ -239,6 +241,84 @@ class BarcodeUnassign(APIView):
|
||||
})
|
||||
|
||||
|
||||
class BarcodePOReceive(APIView):
|
||||
"""Endpoint for handling receiving parts by scanning their barcode.
|
||||
|
||||
Barcode data are decoded by the client application,
|
||||
and sent to this endpoint (as a JSON object) for validation.
|
||||
|
||||
The barcode should follow a third-party barcode format (e.g. Digikey)
|
||||
and ideally contain order_number and quantity information.
|
||||
|
||||
The following parameters are available:
|
||||
|
||||
- barcode: The raw barcode data (required)
|
||||
- purchase_order: The purchase order containing the item to receive (optional)
|
||||
- location: The destination location for the received item (optional)
|
||||
"""
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Respond to a barcode POST request."""
|
||||
|
||||
data = request.data
|
||||
if not (barcode_data := data.get("barcode")):
|
||||
raise ValidationError({"barcode": _("Missing barcode data")})
|
||||
|
||||
purchase_order = None
|
||||
if purchase_order_pk := data.get("purchase_order"):
|
||||
purchase_order = PurchaseOrder.objects.filter(pk=purchase_order_pk).first()
|
||||
if not purchase_order:
|
||||
raise ValidationError({"purchase_order": _("Invalid purchase order")})
|
||||
|
||||
location = None
|
||||
if (location_pk := data.get("location")):
|
||||
location = StockLocation.objects.get(pk=location_pk)
|
||||
if not location:
|
||||
raise ValidationError({"location": _("Invalid stock location")})
|
||||
|
||||
plugins = registry.with_mixin("barcode")
|
||||
|
||||
# Look for a barcode plugin which knows how to deal with this barcode
|
||||
plugin = None
|
||||
response = {}
|
||||
|
||||
internal_barcode_plugin = next(filter(
|
||||
lambda plugin: plugin.name == "InvenTreeBarcode", plugins))
|
||||
if internal_barcode_plugin.scan(barcode_data):
|
||||
response["error"] = _("Item has already been received")
|
||||
raise ValidationError(response)
|
||||
|
||||
for current_plugin in plugins:
|
||||
result = current_plugin.scan_receive_item(
|
||||
barcode_data,
|
||||
request.user,
|
||||
purchase_order=purchase_order,
|
||||
location=location,
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
plugin = current_plugin
|
||||
response = result
|
||||
break
|
||||
|
||||
response["plugin"] = plugin.name if plugin else None
|
||||
response["barcode_data"] = barcode_data
|
||||
response["barcode_hash"] = hash_barcode(barcode_data)
|
||||
|
||||
# A plugin has not been found!
|
||||
if plugin is None:
|
||||
response["error"] = _("Invalid supplier barcode")
|
||||
raise ValidationError(response)
|
||||
elif "error" in response:
|
||||
raise ValidationError(response)
|
||||
else:
|
||||
return Response(response)
|
||||
|
||||
|
||||
barcode_api_urls = [
|
||||
# Link a third-party barcode to an item (e.g. Part / StockItem / etc)
|
||||
path('link/', BarcodeAssign.as_view(), name='api-barcode-link'),
|
||||
@ -246,6 +326,9 @@ barcode_api_urls = [
|
||||
# Unlink a third-pary barcode from an item
|
||||
path('unlink/', BarcodeUnassign.as_view(), name='api-barcode-unlink'),
|
||||
|
||||
# Receive a purchase order item by scanning its barcode
|
||||
path("po-receive/", BarcodePOReceive.as_view(), name="api-barcode-po-receive"),
|
||||
|
||||
# Catch-all performs barcode 'scan'
|
||||
re_path(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'),
|
||||
]
|
||||
|
@ -1,5 +1,22 @@
|
||||
"""Plugin mixin classes for barcode plugin."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import F
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from company.models import Company, SupplierPart
|
||||
from order.models import PurchaseOrder, PurchaseOrderStatus
|
||||
from plugin.base.integration.SettingsMixin import SettingsMixin
|
||||
from stock.models import StockLocation
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class BarcodeMixin:
|
||||
"""Mixin that enables barcode handling.
|
||||
@ -36,3 +53,334 @@ class BarcodeMixin:
|
||||
Default return value is None
|
||||
"""
|
||||
return None
|
||||
|
||||
def scan_receive_item(self, barcode_data, user, purchase_order=None, location=None):
|
||||
"""Scan a barcode to receive a purchase order item.
|
||||
|
||||
It's recommended to use the receive_purchase_order_item method to return from this function.
|
||||
|
||||
Returns:
|
||||
None if the barcode_data could not be parsed.
|
||||
|
||||
A dict object containing:
|
||||
- on success:
|
||||
a "success" message and the received "lineitem"
|
||||
- on partial success (if there's missing information):
|
||||
an "action_required" message and the matched, but not yet received "lineitem"
|
||||
- on failure:
|
||||
an "error" message
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def parse_ecia_barcode2d(barcode_data: str | list[str]) -> dict[str, str]:
|
||||
"""Parse a standard ECIA 2D barcode, according to https://www.ecianow.org/assets/docs/ECIA_Specifications.pdf"""
|
||||
|
||||
if not isinstance(barcode_data, str):
|
||||
data_split = barcode_data
|
||||
elif not (data_split := BarcodeMixin.parse_isoiec_15434_barcode2d(barcode_data)):
|
||||
return None
|
||||
|
||||
barcode_fields = {}
|
||||
for entry in data_split:
|
||||
for identifier, field_name in ECIA_DATA_IDENTIFIER_MAP.items():
|
||||
if entry.startswith(identifier):
|
||||
barcode_fields[field_name] = entry[len(identifier):]
|
||||
break
|
||||
|
||||
return barcode_fields
|
||||
|
||||
@staticmethod
|
||||
def parse_isoiec_15434_barcode2d(barcode_data: str) -> list[str]:
|
||||
"""Parse a ISO/IEC 15434 bardode, returning the split data section."""
|
||||
HEADER = "[)>\x1E06\x1D"
|
||||
TRAILER = "\x1E\x04"
|
||||
|
||||
# some old mouser barcodes start with this messed up header
|
||||
OLD_MOUSER_HEADER = ">[)>06\x1D"
|
||||
if barcode_data.startswith(OLD_MOUSER_HEADER):
|
||||
barcode_data = barcode_data.replace(OLD_MOUSER_HEADER, HEADER, 1)
|
||||
|
||||
# most barcodes don't include the trailer, because "why would you stick to
|
||||
# the standard, right?" so we only check for the header here
|
||||
if not barcode_data.startswith(HEADER):
|
||||
return
|
||||
|
||||
actual_data = barcode_data.split(HEADER, 1)[1].rsplit(TRAILER, 1)[0]
|
||||
|
||||
return actual_data.split("\x1D")
|
||||
|
||||
@staticmethod
|
||||
def get_supplier_parts(sku: str, supplier: Company = None, mpn: str = None):
|
||||
"""Get a supplier part from SKU or by supplier and MPN."""
|
||||
if not (sku or supplier or mpn):
|
||||
return SupplierPart.objects.none()
|
||||
|
||||
supplier_parts = SupplierPart.objects.all()
|
||||
|
||||
if sku:
|
||||
supplier_parts = supplier_parts.filter(SKU__iexact=sku)
|
||||
if len(supplier_parts) == 1:
|
||||
return supplier_parts
|
||||
|
||||
if supplier:
|
||||
supplier_parts = supplier_parts.filter(supplier=supplier.pk)
|
||||
if len(supplier_parts) == 1:
|
||||
return supplier_parts
|
||||
|
||||
if mpn:
|
||||
supplier_parts = SupplierPart.objects.filter(manufacturer_part__MPN__iexact=mpn)
|
||||
if len(supplier_parts) == 1:
|
||||
return supplier_parts
|
||||
|
||||
logger.warning(
|
||||
"Found %d supplier parts for SKU '%s', supplier '%s', MPN '%s'",
|
||||
supplier_parts.count(),
|
||||
sku,
|
||||
supplier.name if supplier else None,
|
||||
mpn,
|
||||
)
|
||||
|
||||
return supplier_parts
|
||||
|
||||
@staticmethod
|
||||
def receive_purchase_order_item(
|
||||
supplier_part: SupplierPart,
|
||||
user: User,
|
||||
quantity: Decimal | str = None,
|
||||
order_number: str = None,
|
||||
purchase_order: PurchaseOrder = None,
|
||||
location: StockLocation = None,
|
||||
barcode: str = None,
|
||||
) -> dict:
|
||||
"""Try to receive a purchase order item.
|
||||
|
||||
Returns:
|
||||
A dict object containing:
|
||||
- on success: a "success" message
|
||||
- on partial success: the "lineitem" with quantity and location (both can be None)
|
||||
- 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:
|
||||
try:
|
||||
quantity = Decimal(quantity)
|
||||
except InvalidOperation:
|
||||
logger.warning("Failed to parse quantity '%s'", quantity)
|
||||
quantity = None
|
||||
|
||||
# find incomplete line_items that match the supplier_part
|
||||
line_items = purchase_order.lines.filter(
|
||||
part=supplier_part.pk, quantity__gt=F("received"))
|
||||
if len(line_items) == 1 or not quantity:
|
||||
line_item = line_items[0]
|
||||
else:
|
||||
# if there are multiple line items and the barcode contains a quantity:
|
||||
# 1. return the first line_item where line_item.quantity == quantity
|
||||
# 2. return the first line_item where line_item.quantity > quantity
|
||||
# 3. return the first line_item
|
||||
for line_item in line_items:
|
||||
if line_item.quantity == quantity:
|
||||
break
|
||||
else:
|
||||
for line_item in line_items:
|
||||
if line_item.quantity > quantity:
|
||||
break
|
||||
else:
|
||||
line_item = line_items.first()
|
||||
|
||||
if not line_item:
|
||||
return {"error": _("Failed to find pending line item for supplier part")}
|
||||
|
||||
no_stock_locations = False
|
||||
if not location:
|
||||
# try to guess the destination were the stock_part should go
|
||||
# 1. check if it's defined on the line_item
|
||||
# 2. check if it's defined on the part
|
||||
# 3. check if there's 1 or 0 stock locations defined in InvenTree
|
||||
# -> assume all stock is going into that location (or no location)
|
||||
if location := line_item.destination:
|
||||
pass
|
||||
elif location := supplier_part.part.get_default_location():
|
||||
pass
|
||||
elif StockLocation.objects.count() <= 1:
|
||||
if not (location := StockLocation.objects.first()):
|
||||
no_stock_locations = True
|
||||
|
||||
response = {
|
||||
"lineitem": {
|
||||
"pk": line_item.pk,
|
||||
"purchase_order": purchase_order.pk,
|
||||
}
|
||||
}
|
||||
|
||||
if quantity:
|
||||
response["lineitem"]["quantity"] = quantity
|
||||
if location:
|
||||
response["lineitem"]["location"] = location.pk
|
||||
|
||||
# if either the quantity is missing or no location is defined/found
|
||||
# -> return the line_item found, so the client can gather the missing
|
||||
# information and complete the action with an 'api-po-receive' call
|
||||
if not quantity or (not location and not no_stock_locations):
|
||||
response["action_required"] = _("Further information required to receive line item")
|
||||
return response
|
||||
|
||||
purchase_order.receive_line_item(
|
||||
line_item,
|
||||
location,
|
||||
quantity,
|
||||
user,
|
||||
barcode=barcode,
|
||||
)
|
||||
|
||||
response["success"] = _("Received purchase order line item")
|
||||
return response
|
||||
|
||||
|
||||
@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):
|
||||
"""Mixin that provides default implementations for scan functions for supplier barcodes.
|
||||
|
||||
Custom supplier barcode plugins should use this mixin and implement the
|
||||
parse_supplier_barcode_data function.
|
||||
"""
|
||||
|
||||
def parse_supplier_barcode_data(self, barcode_data) -> SupplierBarcodeData | None:
|
||||
"""Get supplier_part and other barcode_fields from barcode data.
|
||||
|
||||
Returns:
|
||||
None if the barcode_data is not from a valid barcode of the supplier.
|
||||
|
||||
A SupplierBarcodeData object containing the SKU, MPN, quantity and order number
|
||||
if available.
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
def scan(self, barcode_data):
|
||||
"""Try to match a supplier barcode to a supplier part."""
|
||||
|
||||
if not (parsed := self.parse_supplier_barcode_data(barcode_data)):
|
||||
return None
|
||||
if parsed.SKU is None and parsed.MPN is None:
|
||||
return None
|
||||
|
||||
supplier_parts = self.get_supplier_parts(parsed.SKU, self.get_supplier(), parsed.MPN)
|
||||
if len(supplier_parts) > 1:
|
||||
return {"error": _("Found multiple matching supplier parts for barcode")}
|
||||
elif not supplier_parts:
|
||||
return None
|
||||
supplier_part = supplier_parts[0]
|
||||
|
||||
data = {
|
||||
"pk": supplier_part.pk,
|
||||
"api_url": f"{SupplierPart.get_api_url()}{supplier_part.pk}/",
|
||||
"web_url": supplier_part.get_absolute_url(),
|
||||
}
|
||||
|
||||
return {SupplierPart.barcode_model_type(): data}
|
||||
|
||||
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."""
|
||||
|
||||
if not (parsed := self.parse_supplier_barcode_data(barcode_data)):
|
||||
return None
|
||||
if parsed.SKU is None and parsed.MPN is None:
|
||||
return None
|
||||
|
||||
supplier_parts = self.get_supplier_parts(parsed.SKU, self.get_supplier(), parsed.MPN)
|
||||
if len(supplier_parts) > 1:
|
||||
return {"error": _("Found multiple matching supplier parts for barcode")}
|
||||
elif not supplier_parts:
|
||||
return None
|
||||
supplier_part = supplier_parts[0]
|
||||
|
||||
return self.receive_purchase_order_item(
|
||||
supplier_part,
|
||||
user,
|
||||
quantity=parsed.quantity,
|
||||
order_number=parsed.order_number,
|
||||
purchase_order=purchase_order,
|
||||
location=location,
|
||||
barcode=barcode_data,
|
||||
)
|
||||
|
||||
def get_supplier(self) -> Company | None:
|
||||
"""Get the supplier for the SUPPLIER_ID set in the plugin settings.
|
||||
|
||||
If it's not defined, try to guess it and set it if possible.
|
||||
"""
|
||||
|
||||
if not isinstance(self, SettingsMixin):
|
||||
return None
|
||||
|
||||
if supplier_pk := self.get_setting("SUPPLIER_ID"):
|
||||
if (supplier := Company.objects.get(pk=supplier_pk)):
|
||||
return supplier
|
||||
else:
|
||||
logger.error(
|
||||
"No company with pk %d (set \"SUPPLIER_ID\" setting to a valid value)",
|
||||
supplier_pk
|
||||
)
|
||||
return None
|
||||
|
||||
if not (supplier_name := getattr(self, "DEFAULT_SUPPLIER_NAME", None)):
|
||||
return None
|
||||
|
||||
suppliers = Company.objects.filter(name__icontains=supplier_name, is_supplier=True)
|
||||
if len(suppliers) != 1:
|
||||
return None
|
||||
self.set_setting("SUPPLIER_ID", suppliers.first().pk)
|
||||
|
||||
return suppliers.first()
|
||||
|
||||
|
||||
# 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
|
||||
}
|
||||
|
0
InvenTree/plugin/builtin/suppliers/__init__.py
Normal file
0
InvenTree/plugin/builtin/suppliers/__init__.py
Normal file
49
InvenTree/plugin/builtin/suppliers/digikey.py
Normal file
49
InvenTree/plugin/builtin/suppliers/digikey.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""The DigiKeyPlugin is meant to integrate the DigiKey API into Inventree.
|
||||
|
||||
This plugin can currently only match DigiKey barcodes to supplier parts.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
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
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class DigiKeyPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
||||
"""Plugin to integrate the DigiKey API into Inventree."""
|
||||
|
||||
NAME = "DigiKeyPlugin"
|
||||
TITLE = _("Supplier Integration - DigiKey")
|
||||
DESCRIPTION = _("Provides support for scanning DigiKey barcodes")
|
||||
VERSION = "1.0.0"
|
||||
AUTHOR = _("InvenTree contributors")
|
||||
|
||||
DEFAULT_SUPPLIER_NAME = "DigiKey"
|
||||
SETTINGS = {
|
||||
"SUPPLIER_ID": {
|
||||
"name": _("Supplier"),
|
||||
"description": _("The Supplier which acts as 'DigiKey'"),
|
||||
"model": "company.company",
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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"),
|
||||
)
|
55
InvenTree/plugin/builtin/suppliers/lcsc.py
Normal file
55
InvenTree/plugin/builtin/suppliers/lcsc.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""The LCSCPlugin is meant to integrate the LCSC API into Inventree.
|
||||
|
||||
This plugin can currently only match LCSC barcodes to supplier parts.
|
||||
"""
|
||||
|
||||
import logging
|
||||
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
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class LCSCPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
||||
"""Plugin to integrate the LCSC API into Inventree."""
|
||||
|
||||
NAME = "LCSCPlugin"
|
||||
TITLE = _("Supplier Integration - LCSC")
|
||||
DESCRIPTION = _("Provides support for scanning LCSC barcodes")
|
||||
VERSION = "1.0.0"
|
||||
AUTHOR = _("InvenTree contributors")
|
||||
|
||||
DEFAULT_SUPPLIER_NAME = "LCSC"
|
||||
SETTINGS = {
|
||||
"SUPPLIER_ID": {
|
||||
"name": _("Supplier"),
|
||||
"description": _("The Supplier which acts as 'LCSC'"),
|
||||
"model": "company.company",
|
||||
}
|
||||
}
|
||||
|
||||
def parse_supplier_barcode_data(self, barcode_data):
|
||||
"""Get supplier_part and barcode_fields from LCSC QR-Code."""
|
||||
|
||||
if not isinstance(barcode_data, str):
|
||||
return None
|
||||
|
||||
if not (match := LCSC_BARCODE_REGEX.fullmatch(barcode_data)):
|
||||
return None
|
||||
|
||||
barcode_fields = dict(pair.split(":") for pair in match.group(1).split(","))
|
||||
|
||||
return SupplierBarcodeData(
|
||||
SKU=barcode_fields.get("pc"),
|
||||
MPN=barcode_fields.get("pm"),
|
||||
quantity=barcode_fields.get("qty"),
|
||||
order_number=barcode_fields.get("on"),
|
||||
)
|
||||
|
||||
|
||||
LCSC_BARCODE_REGEX = re.compile(r"^{((?:[^:,]+:[^:,]*,)*(?:[^:,]+:[^:,]*))}$")
|
49
InvenTree/plugin/builtin/suppliers/mouser.py
Normal file
49
InvenTree/plugin/builtin/suppliers/mouser.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""The MouserPlugin is meant to integrate the Mouser API into Inventree.
|
||||
|
||||
This plugin currently only match Mouser barcodes to supplier parts.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
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
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class MouserPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
||||
"""Plugin to integrate the Mouser API into Inventree."""
|
||||
|
||||
NAME = "MouserPlugin"
|
||||
TITLE = _("Supplier Integration - Mouser")
|
||||
DESCRIPTION = _("Provides support for scanning Mouser barcodes")
|
||||
VERSION = "1.0.0"
|
||||
AUTHOR = _("InvenTree contributors")
|
||||
|
||||
DEFAULT_SUPPLIER_NAME = "Mouser"
|
||||
SETTINGS = {
|
||||
"SUPPLIER_ID": {
|
||||
"name": _("Supplier"),
|
||||
"description": _("The Supplier which acts as 'Mouser'"),
|
||||
"model": "company.company",
|
||||
}
|
||||
}
|
||||
|
||||
def parse_supplier_barcode_data(self, barcode_data):
|
||||
"""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"),
|
||||
)
|
308
InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py
Normal file
308
InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py
Normal file
@ -0,0 +1,308 @@
|
||||
"""Tests barcode parsing for all suppliers."""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from company.models import Company, ManufacturerPart, SupplierPart
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||
from order.models import PurchaseOrder, PurchaseOrderLineItem
|
||||
from part.models import Part
|
||||
from stock.models import StockItem, StockLocation
|
||||
|
||||
|
||||
class SupplierBarcodeTests(InvenTreeAPITestCase):
|
||||
"""Tests barcode parsing for all suppliers."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
"""Create supplier parts for barcodes."""
|
||||
super().setUpTestData()
|
||||
|
||||
part = Part.objects.create(name="Test Part", description="Test Part")
|
||||
|
||||
manufacturer = Company.objects.create(
|
||||
name="Test Manufacturer", is_manufacturer=True)
|
||||
|
||||
mpart1 = ManufacturerPart.objects.create(
|
||||
part=part, manufacturer=manufacturer, MPN="MC34063ADR")
|
||||
mpart2 = ManufacturerPart.objects.create(
|
||||
part=part, manufacturer=manufacturer, MPN="LDK320ADU33R")
|
||||
|
||||
supplier = Company.objects.create(name="Supplier", is_supplier=True)
|
||||
mouser = Company.objects.create(name="Mouser Test", is_supplier=True)
|
||||
|
||||
supplier_parts = [
|
||||
SupplierPart(SKU="296-LM358BIDDFRCT-ND", part=part, supplier=supplier),
|
||||
SupplierPart(SKU="1", part=part, manufacturer_part=mpart1, supplier=mouser),
|
||||
SupplierPart(SKU="2", part=part, manufacturer_part=mpart2, supplier=mouser),
|
||||
SupplierPart(SKU="C312270", part=part, supplier=supplier),
|
||||
SupplierPart(SKU="WBP-302", part=part, supplier=supplier),
|
||||
]
|
||||
|
||||
SupplierPart.objects.bulk_create(supplier_parts)
|
||||
|
||||
def test_digikey_barcode(self):
|
||||
"""Test digikey barcode."""
|
||||
|
||||
url = reverse("api-barcode-scan")
|
||||
result = self.post(url, data={"barcode": DIGIKEY_BARCODE})
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert "pk" in supplier_part_data
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert supplier_part.SKU == "296-LM358BIDDFRCT-ND"
|
||||
|
||||
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})
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert "pk" in supplier_part_data
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert 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})
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert "pk" in supplier_part_data
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert 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})
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert supplier_part_data is not None
|
||||
|
||||
assert "pk" in supplier_part_data
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert 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})
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert supplier_part_data is not None
|
||||
|
||||
assert "pk" in supplier_part_data
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert 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})
|
||||
|
||||
supplier_part_data = result.data.get("supplierpart")
|
||||
assert supplier_part_data is not None
|
||||
|
||||
assert "pk" in supplier_part_data
|
||||
supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
|
||||
assert supplier_part.SKU == "WBP-302"
|
||||
|
||||
|
||||
class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
|
||||
"""Tests barcode scanning to receive a purchase order item."""
|
||||
|
||||
def setUp(self):
|
||||
"""Create supplier part and purchase_order."""
|
||||
super().setUp()
|
||||
|
||||
part = Part.objects.create(name="Test Part", description="Test Part")
|
||||
supplier = Company.objects.create(name="Supplier", is_supplier=True)
|
||||
manufacturer = Company.objects.create(
|
||||
name="Test Manufacturer", is_manufacturer=True)
|
||||
|
||||
mouser = Company.objects.create(name="Mouser Test", is_supplier=True)
|
||||
mpart = ManufacturerPart.objects.create(
|
||||
part=part, manufacturer=manufacturer, MPN="MC34063ADR")
|
||||
|
||||
self.purchase_order1 = PurchaseOrder.objects.create(
|
||||
supplier_reference="72991337", supplier=supplier)
|
||||
supplier_parts1 = [
|
||||
SupplierPart(SKU=f"1_{i}", part=part, supplier=supplier)
|
||||
for i in range(6)
|
||||
]
|
||||
supplier_parts1.insert(
|
||||
2, SupplierPart(SKU="296-LM358BIDDFRCT-ND", part=part, supplier=supplier))
|
||||
for supplier_part in supplier_parts1:
|
||||
supplier_part.save()
|
||||
self.purchase_order1.add_line_item(supplier_part, 8)
|
||||
|
||||
self.purchase_order2 = PurchaseOrder.objects.create(
|
||||
reference="P0-1337", supplier=mouser)
|
||||
self.purchase_order2.place_order()
|
||||
supplier_parts2 = [
|
||||
SupplierPart(SKU=f"2_{i}", part=part, supplier=mouser)
|
||||
for i in range(6)
|
||||
]
|
||||
supplier_parts2.insert(
|
||||
3, SupplierPart(SKU="42", part=part, manufacturer_part=mpart, supplier=mouser))
|
||||
for supplier_part in supplier_parts2:
|
||||
supplier_part.save()
|
||||
self.purchase_order2.add_line_item(supplier_part, 5)
|
||||
|
||||
def test_receive(self):
|
||||
"""Test receiving an item from a barcode."""
|
||||
|
||||
url = reverse("api-barcode-po-receive")
|
||||
|
||||
result1 = self.post(url, data={"barcode": DIGIKEY_BARCODE})
|
||||
assert result1.status_code == 400
|
||||
assert result1.data["error"].startswith("Failed to find placed purchase order")
|
||||
|
||||
self.purchase_order1.place_order()
|
||||
|
||||
result2 = self.post(url, data={"barcode": DIGIKEY_BARCODE})
|
||||
assert result2.status_code == 200
|
||||
assert "success" in result2.data
|
||||
|
||||
result3 = self.post(url, data={"barcode": DIGIKEY_BARCODE})
|
||||
assert result3.status_code == 400
|
||||
assert result3.data["error"].startswith(
|
||||
"Item has already been received")
|
||||
|
||||
result4 = self.post(url, data={"barcode": DIGIKEY_BARCODE[:-1]})
|
||||
assert result4.status_code == 400
|
||||
assert result4.data["error"].startswith(
|
||||
"Failed to find pending line item for supplier part")
|
||||
|
||||
result5 = self.post(reverse("api-barcode-scan"), data={"barcode": DIGIKEY_BARCODE})
|
||||
assert result5.status_code == 200
|
||||
stock_item = StockItem.objects.get(pk=result5.data["stockitem"]["pk"])
|
||||
assert stock_item.supplier_part.SKU == "296-LM358BIDDFRCT-ND"
|
||||
assert stock_item.quantity == 10
|
||||
assert stock_item.location is None
|
||||
|
||||
def test_receive_custom_order_number(self):
|
||||
"""Test receiving an item from a barcode with a custom order number."""
|
||||
|
||||
url = reverse("api-barcode-po-receive")
|
||||
result1 = self.post(url, data={"barcode": MOUSER_BARCODE})
|
||||
assert "success" in result1.data
|
||||
|
||||
result2 = self.post(reverse("api-barcode-scan"), data={"barcode": MOUSER_BARCODE})
|
||||
stock_item = StockItem.objects.get(pk=result2.data["stockitem"]["pk"])
|
||||
assert stock_item.supplier_part.SKU == "42"
|
||||
assert stock_item.supplier_part.manufacturer_part.MPN == "MC34063ADR"
|
||||
assert stock_item.quantity == 3
|
||||
assert stock_item.location is None
|
||||
|
||||
def test_receive_one_stock_location(self):
|
||||
"""Test receiving an item when only one stock location exists"""
|
||||
|
||||
stock_location = StockLocation.objects.create(name="Test Location")
|
||||
|
||||
url = reverse("api-barcode-po-receive")
|
||||
result1 = self.post(url, data={"barcode": MOUSER_BARCODE})
|
||||
assert "success" in result1.data
|
||||
|
||||
result2 = self.post(reverse("api-barcode-scan"), data={"barcode": MOUSER_BARCODE})
|
||||
stock_item = StockItem.objects.get(pk=result2.data["stockitem"]["pk"])
|
||||
assert stock_item.location == stock_location
|
||||
|
||||
def test_receive_default_line_item_location(self):
|
||||
"""Test receiving an item into the default line_item location"""
|
||||
|
||||
StockLocation.objects.create(name="Test Location 1")
|
||||
stock_location2 = StockLocation.objects.create(name="Test Location 2")
|
||||
|
||||
line_item = PurchaseOrderLineItem.objects.filter(part__SKU="42")[0]
|
||||
line_item.destination = stock_location2
|
||||
line_item.save()
|
||||
|
||||
url = reverse("api-barcode-po-receive")
|
||||
result1 = self.post(url, data={"barcode": MOUSER_BARCODE})
|
||||
assert "success" in result1.data
|
||||
|
||||
result2 = self.post(reverse("api-barcode-scan"), data={"barcode": MOUSER_BARCODE})
|
||||
stock_item = StockItem.objects.get(pk=result2.data["stockitem"]["pk"])
|
||||
assert stock_item.location == stock_location2
|
||||
|
||||
def test_receive_default_part_location(self):
|
||||
"""Test receiving an item into the default part location"""
|
||||
|
||||
StockLocation.objects.create(name="Test Location 1")
|
||||
stock_location2 = StockLocation.objects.create(name="Test Location 2")
|
||||
|
||||
part = Part.objects.all()[0]
|
||||
part.default_location = stock_location2
|
||||
part.save()
|
||||
|
||||
url = reverse("api-barcode-po-receive")
|
||||
result1 = self.post(url, data={"barcode": MOUSER_BARCODE})
|
||||
assert "success" in result1.data
|
||||
|
||||
result2 = self.post(reverse("api-barcode-scan"), data={"barcode": MOUSER_BARCODE})
|
||||
stock_item = StockItem.objects.get(pk=result2.data["stockitem"]["pk"])
|
||||
assert stock_item.location == stock_location2
|
||||
|
||||
def test_receive_specific_order_and_location(self):
|
||||
"""Test receiving an item from a specific order into a specific location"""
|
||||
|
||||
StockLocation.objects.create(name="Test Location 1")
|
||||
stock_location2 = StockLocation.objects.create(name="Test Location 2")
|
||||
|
||||
url = reverse("api-barcode-po-receive")
|
||||
barcode = MOUSER_BARCODE.replace("\x1dKP0-1337", "")
|
||||
result1 = self.post(url, data={
|
||||
"barcode": barcode,
|
||||
"purchase_order": self.purchase_order2.pk,
|
||||
"location": stock_location2.pk,
|
||||
})
|
||||
assert "success" in result1.data
|
||||
|
||||
result2 = self.post(reverse("api-barcode-scan"), data={"barcode": barcode})
|
||||
stock_item = StockItem.objects.get(pk=result2.data["stockitem"]["pk"])
|
||||
assert stock_item.location == stock_location2
|
||||
|
||||
def test_receive_missing_quantity(self):
|
||||
"""Test receiving an with missing quantity information"""
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
DIGIKEY_BARCODE = (
|
||||
"[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\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"
|
||||
)
|
||||
|
||||
MOUSER_BARCODE_OLD = (
|
||||
">[)>06\x1dK21421337\x1d14K033\x1d1PLDK320ADU33R\x1dQ32\x1d11K060931337\x1d"
|
||||
"4LCN\x1d1VSTMicro"
|
||||
)
|
||||
|
||||
LCSC_BARCODE = (
|
||||
"{pbn:PICK2009291337,on:SO2009291337,pc:C312270,pm:ST-1-102-A01-T000-RS,qty"
|
||||
":2,mc:,cc:1,pdi:34421807}"
|
||||
)
|
||||
|
||||
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"
|
74
InvenTree/plugin/builtin/suppliers/tme.py
Normal file
74
InvenTree/plugin/builtin/suppliers/tme.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""The TMEPlugin is meant to integrate the TME API into Inventree.
|
||||
|
||||
This plugin can currently only match TME barcodes to supplier parts.
|
||||
"""
|
||||
|
||||
import logging
|
||||
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
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class TMEPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
|
||||
"""Plugin to integrate the TME API into Inventree."""
|
||||
|
||||
NAME = "TMEPlugin"
|
||||
TITLE = _("Supplier Integration - TME")
|
||||
DESCRIPTION = _("Provides support for scanning TME barcodes")
|
||||
VERSION = "1.0.0"
|
||||
AUTHOR = _("InvenTree contributors")
|
||||
|
||||
DEFAULT_SUPPLIER_NAME = "TME"
|
||||
SETTINGS = {
|
||||
"SUPPLIER_ID": {
|
||||
"name": _("Supplier"),
|
||||
"description": _("The Supplier which acts as 'TME'"),
|
||||
"model": "company.company",
|
||||
}
|
||||
}
|
||||
|
||||
def parse_supplier_barcode_data(self, barcode_data):
|
||||
"""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)
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
if order_number := barcode_fields.get("purchase_order_number"):
|
||||
order_number = order_number.split("/")[0]
|
||||
|
||||
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",
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
from common.notifications import (BulkNotificationMethod,
|
||||
SingleNotificationMethod)
|
||||
from plugin.base.action.mixins import ActionMixin
|
||||
from plugin.base.barcodes.mixins import BarcodeMixin
|
||||
from plugin.base.barcodes.mixins import BarcodeMixin, SupplierBarcodeMixin
|
||||
from plugin.base.event.mixins import EventMixin
|
||||
from plugin.base.integration.APICallMixin import APICallMixin
|
||||
from plugin.base.integration.AppMixin import AppMixin
|
||||
@ -33,6 +33,7 @@ __all__ = [
|
||||
'PanelMixin',
|
||||
'ActionMixin',
|
||||
'BarcodeMixin',
|
||||
'SupplierBarcodeMixin',
|
||||
'LocateMixin',
|
||||
'ValidationMixin',
|
||||
'SingleNotificationMethod',
|
||||
|
@ -26,3 +26,16 @@ The barcode is tested as follows, in decreasing order of priority:
|
||||
|
||||
!!! tip "Plugin Loading Order"
|
||||
The first custom plugin to return a result "wins". As the loading order of custom plugins is not defined (or configurable), take special care if you are running multiple plugins which support barcode actions.
|
||||
|
||||
## Builtin Supplier Barcode Plugins
|
||||
|
||||
InvenTree comes with a few builtin supplier plugins, which handle their respective barcode formats.
|
||||
|
||||
Scanning a supplier barcode for a supplied part will link to the corresponding supplier part if the [SKU](../report/context_variables.md#supplierpart) from the barcode could be matched.
|
||||
|
||||
The following suppliers (and barcode formats) are currently supported:
|
||||
|
||||
- DigiKey (2D Data Matrix code)
|
||||
- Mouser (2D Data Matrix code)
|
||||
- LCSC (QR code)
|
||||
- TME (QR code & 2D Data Matrix code)
|
||||
|
Loading…
Reference in New Issue
Block a user