mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Refactoring (#5921)
- Move all supplier barcode functions into SupplierBarcodeMixin - Look for specific mixin class when scanning
This commit is contained in:
parent
cf3d96a265
commit
f42fc77cd5
@ -277,10 +277,14 @@ class BarcodePOReceive(APIView):
|
||||
"""Respond to a barcode POST request."""
|
||||
|
||||
data = request.data
|
||||
|
||||
if not (barcode_data := data.get("barcode")):
|
||||
raise ValidationError({"barcode": _("Missing barcode data")})
|
||||
|
||||
logger.debug("BarcodePOReceive: scanned barcode - '%s'", 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:
|
||||
@ -304,7 +308,11 @@ class BarcodePOReceive(APIView):
|
||||
response["error"] = _("Item has already been received")
|
||||
raise ValidationError(response)
|
||||
|
||||
# Now, look just for "supplier-barcode" plugins
|
||||
plugins = registry.with_mixin("supplier-barcode")
|
||||
|
||||
for current_plugin in plugins:
|
||||
|
||||
result = current_plugin.scan_receive_item(
|
||||
barcode_data,
|
||||
request.user,
|
||||
|
@ -54,32 +54,124 @@ class BarcodeMixin:
|
||||
"""
|
||||
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.
|
||||
@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 __init__(self):
|
||||
"""Register mixin."""
|
||||
super().__init__()
|
||||
self.add_mixin('supplier-barcode', True, __class__)
|
||||
|
||||
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 could not be parsed.
|
||||
None if the barcode_data is not from a valid barcode of the supplier.
|
||||
|
||||
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
|
||||
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()
|
||||
|
||||
@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)):
|
||||
elif not (data_split := SupplierBarcodeMixin.parse_isoiec_15434_barcode2d(barcode_data)):
|
||||
return None
|
||||
|
||||
barcode_fields = {}
|
||||
@ -260,112 +352,6 @@ class BarcodeMixin:
|
||||
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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user