Refactoring (#5921)

- Move all supplier barcode functions into SupplierBarcodeMixin
- Look for specific mixin class when scanning
This commit is contained in:
Oliver 2023-11-15 14:38:58 +11:00 committed by GitHub
parent cf3d96a265
commit f42fc77cd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 112 additions and 118 deletions

View File

@ -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,

View File

@ -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 = {