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."""
|
"""Respond to a barcode POST request."""
|
||||||
|
|
||||||
data = request.data
|
data = request.data
|
||||||
|
|
||||||
if not (barcode_data := data.get("barcode")):
|
if not (barcode_data := data.get("barcode")):
|
||||||
raise ValidationError({"barcode": _("Missing barcode data")})
|
raise ValidationError({"barcode": _("Missing barcode data")})
|
||||||
|
|
||||||
|
logger.debug("BarcodePOReceive: scanned barcode - '%s'", barcode_data)
|
||||||
|
|
||||||
purchase_order = None
|
purchase_order = None
|
||||||
|
|
||||||
if purchase_order_pk := data.get("purchase_order"):
|
if purchase_order_pk := data.get("purchase_order"):
|
||||||
purchase_order = PurchaseOrder.objects.filter(pk=purchase_order_pk).first()
|
purchase_order = PurchaseOrder.objects.filter(pk=purchase_order_pk).first()
|
||||||
if not purchase_order:
|
if not purchase_order:
|
||||||
@ -304,7 +308,11 @@ class BarcodePOReceive(APIView):
|
|||||||
response["error"] = _("Item has already been received")
|
response["error"] = _("Item has already been received")
|
||||||
raise ValidationError(response)
|
raise ValidationError(response)
|
||||||
|
|
||||||
|
# Now, look just for "supplier-barcode" plugins
|
||||||
|
plugins = registry.with_mixin("supplier-barcode")
|
||||||
|
|
||||||
for current_plugin in plugins:
|
for current_plugin in plugins:
|
||||||
|
|
||||||
result = current_plugin.scan_receive_item(
|
result = current_plugin.scan_receive_item(
|
||||||
barcode_data,
|
barcode_data,
|
||||||
request.user,
|
request.user,
|
||||||
|
@ -54,32 +54,124 @@ class BarcodeMixin:
|
|||||||
"""
|
"""
|
||||||
return 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.
|
@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:
|
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:
|
A SupplierBarcodeData object containing the SKU, MPN, quantity and order number
|
||||||
- on success:
|
if available.
|
||||||
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
|
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
|
@staticmethod
|
||||||
def parse_ecia_barcode2d(barcode_data: str | list[str]) -> dict[str, str]:
|
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"""
|
"""Parse a standard ECIA 2D barcode, according to https://www.ecianow.org/assets/docs/ECIA_Specifications.pdf"""
|
||||||
|
|
||||||
if not isinstance(barcode_data, str):
|
if not isinstance(barcode_data, str):
|
||||||
data_split = barcode_data
|
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
|
return None
|
||||||
|
|
||||||
barcode_fields = {}
|
barcode_fields = {}
|
||||||
@ -260,112 +352,6 @@ class BarcodeMixin:
|
|||||||
return response
|
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
|
# Map ECIA Data Identifier to human readable identifier
|
||||||
# The following identifiers haven't been implemented: 3S, 4S, 5S, S
|
# The following identifiers haven't been implemented: 3S, 4S, 5S, S
|
||||||
ECIA_DATA_IDENTIFIER_MAP = {
|
ECIA_DATA_IDENTIFIER_MAP = {
|
||||||
|
Loading…
Reference in New Issue
Block a user