mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +00:00 
			
		
		
		
	Refactor existing barcode API endpoints (#5937)
* Refactor existing barcode API endpoints - Expose fields using proper DRF serializers - API endpoints are now self documenting - Validation is handled by serializer models - Serializers and endpoints are extensible - Extended existing unit tests * Catch errors if db not yet loaded * Tweak unit tests
This commit is contained in:
		@@ -349,7 +349,7 @@ def MakeBarcode(cls_name, object_pk: int, object_data=None, **kwargs):
 | 
				
			|||||||
        object_data['id'] = object_pk
 | 
					        object_data['id'] = object_pk
 | 
				
			||||||
        data[cls_name] = object_data
 | 
					        data[cls_name] = object_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return json.dumps(data, sort_keys=True)
 | 
					    return str(json.dumps(data, sort_keys=True))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def GetExportFormats():
 | 
					def GetExportFormats():
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -228,7 +228,11 @@ def getModelsWithMixin(mixin_class) -> list:
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    from django.contrib.contenttypes.models import ContentType
 | 
					    from django.contrib.contenttypes.models import ContentType
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db_models = [x.model_class() for x in ContentType.objects.all() if x is not None]
 | 
					    try:
 | 
				
			||||||
 | 
					        db_models = [x.model_class() for x in ContentType.objects.all() if x is not None]
 | 
				
			||||||
 | 
					    except (OperationalError, ProgrammingError):
 | 
				
			||||||
 | 
					        # Database is likely not yet ready
 | 
				
			||||||
 | 
					        db_models = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return [x for x in db_models if x is not None and issubclass(x, mixin_class)]
 | 
					    return [x for x in db_models if x is not None and issubclass(x, mixin_class)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -522,7 +522,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
 | 
				
			|||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_pending(self):
 | 
					    def is_pending(self):
 | 
				
			||||||
        """Return True if the PurchaseOrder is 'pending'"""
 | 
					        """Return True if the PurchaseOrder is 'pending'"""
 | 
				
			||||||
        return self.status == PurchaseOrderStatus.PENDING
 | 
					        return self.status == PurchaseOrderStatus.PENDING.value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_open(self):
 | 
					    def is_open(self):
 | 
				
			||||||
@@ -536,8 +536,8 @@ class PurchaseOrder(TotalPriceMixin, Order):
 | 
				
			|||||||
        - Status is PENDING
 | 
					        - Status is PENDING
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self.status in [
 | 
					        return self.status in [
 | 
				
			||||||
            PurchaseOrderStatus.PLACED,
 | 
					            PurchaseOrderStatus.PLACED.value,
 | 
				
			||||||
            PurchaseOrderStatus.PENDING
 | 
					            PurchaseOrderStatus.PENDING.value
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @transaction.atomic
 | 
					    @transaction.atomic
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,21 +7,60 @@ from django.utils.translation import gettext_lazy as _
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from rest_framework import permissions
 | 
					from rest_framework import permissions
 | 
				
			||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
 | 
					from rest_framework.exceptions import PermissionDenied, ValidationError
 | 
				
			||||||
 | 
					from rest_framework.generics import CreateAPIView
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework.views import APIView
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from InvenTree.helpers import hash_barcode
 | 
					from InvenTree.helpers import hash_barcode
 | 
				
			||||||
from order.models import PurchaseOrder
 | 
					 | 
				
			||||||
from plugin import registry
 | 
					from plugin import registry
 | 
				
			||||||
from plugin.builtin.barcodes.inventree_barcode import \
 | 
					from plugin.builtin.barcodes.inventree_barcode import \
 | 
				
			||||||
    InvenTreeInternalBarcodePlugin
 | 
					    InvenTreeInternalBarcodePlugin
 | 
				
			||||||
from stock.models import StockLocation
 | 
					 | 
				
			||||||
from users.models import RuleSet
 | 
					from users.models import RuleSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import serializers as barcode_serializers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger('inventree')
 | 
					logger = logging.getLogger('inventree')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BarcodeScan(APIView):
 | 
					class BarcodeView(CreateAPIView):
 | 
				
			||||||
 | 
					    """Custom view class for handling a barcode scan"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Default serializer class (can be overridden)
 | 
				
			||||||
 | 
					    serializer_class = barcode_serializers.BarcodeSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def queryset(self):
 | 
				
			||||||
 | 
					        """This API view does not have a queryset"""
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Default permission classes (can be overridden)
 | 
				
			||||||
 | 
					    permission_classes = [
 | 
				
			||||||
 | 
					        permissions.IsAuthenticated,
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def create(self, request, *args, **kwargs):
 | 
				
			||||||
 | 
					        """Handle create method - override default create"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = self.get_serializer(data=request.data)
 | 
				
			||||||
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        data = serializer.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        barcode = str(data.pop('barcode')).strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self.handle_barcode(barcode, request, **data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle_barcode(self, barcode: str, request, **kwargs):
 | 
				
			||||||
 | 
					        """Handle barcode scan.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Arguments:
 | 
				
			||||||
 | 
					            barcode: Raw barcode value
 | 
				
			||||||
 | 
					            request: HTTP request object
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        kwargs:
 | 
				
			||||||
 | 
					            Any custom fields passed by the specific serializer
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        raise NotImplementedError(f"handle_barcode not implemented for {self.__class__}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BarcodeScan(BarcodeView):
 | 
				
			||||||
    """Endpoint for handling generic barcode scan requests.
 | 
					    """Endpoint for handling generic barcode scan requests.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Barcode data are decoded by the client application,
 | 
					    Barcode data are decoded by the client application,
 | 
				
			||||||
@@ -29,47 +68,29 @@ class BarcodeScan(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    A barcode could follow the internal InvenTree barcode format,
 | 
					    A barcode could follow the internal InvenTree barcode format,
 | 
				
			||||||
    or it could match to a third-party barcode format (e.g. Digikey).
 | 
					    or it could match to a third-party barcode format (e.g. Digikey).
 | 
				
			||||||
 | 
					 | 
				
			||||||
    When a barcode is sent to the server, the following parameters must be provided:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    - barcode: The raw barcode data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    plugins:
 | 
					 | 
				
			||||||
    Third-party barcode formats may be supported using 'plugins'
 | 
					 | 
				
			||||||
    (more information to follow)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    hashing:
 | 
					 | 
				
			||||||
    Barcode hashes are calculated using MD5
 | 
					 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    permission_classes = [
 | 
					    def handle_barcode(self, barcode: str, request, **kwargs):
 | 
				
			||||||
        permissions.IsAuthenticated,
 | 
					        """Perform barcode scan action
 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					        Arguments:
 | 
				
			||||||
        """Respond to a barcode POST request.
 | 
					            barcode: Raw barcode value
 | 
				
			||||||
 | 
					            request: HTTP request object
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Check if required info was provided and then run though the plugin steps or try to match up-
 | 
					        kwargs:
 | 
				
			||||||
 | 
					            Any custom fields passed by the specific serializer
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        data = request.data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        barcode_data = data.get('barcode', None)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not barcode_data:
 | 
					 | 
				
			||||||
            raise ValidationError({'barcode': _('Missing barcode data')})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Note: the default barcode handlers are loaded (and thus run) first
 | 
					        # Note: the default barcode handlers are loaded (and thus run) first
 | 
				
			||||||
        plugins = registry.with_mixin('barcode')
 | 
					        plugins = registry.with_mixin('barcode')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        barcode_hash = hash_barcode(barcode_data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Look for a barcode plugin which knows how to deal with this barcode
 | 
					        # Look for a barcode plugin which knows how to deal with this barcode
 | 
				
			||||||
        plugin = None
 | 
					        plugin = None
 | 
				
			||||||
        response = {}
 | 
					        response = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for current_plugin in plugins:
 | 
					        for current_plugin in plugins:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            result = current_plugin.scan(barcode_data)
 | 
					            result = current_plugin.scan(barcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if result is None:
 | 
					            if result is None:
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
@@ -86,8 +107,8 @@ class BarcodeScan(APIView):
 | 
				
			|||||||
                break
 | 
					                break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response['plugin'] = plugin.name if plugin else None
 | 
					        response['plugin'] = plugin.name if plugin else None
 | 
				
			||||||
        response['barcode_data'] = barcode_data
 | 
					        response['barcode_data'] = barcode
 | 
				
			||||||
        response['barcode_hash'] = barcode_hash
 | 
					        response['barcode_hash'] = hash_barcode(barcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # A plugin has not been found!
 | 
					        # A plugin has not been found!
 | 
				
			||||||
        if plugin is None:
 | 
					        if plugin is None:
 | 
				
			||||||
@@ -99,44 +120,36 @@ class BarcodeScan(APIView):
 | 
				
			|||||||
            return Response(response)
 | 
					            return Response(response)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BarcodeAssign(APIView):
 | 
					class BarcodeAssign(BarcodeView):
 | 
				
			||||||
    """Endpoint for assigning a barcode to a stock item.
 | 
					    """Endpoint for assigning a barcode to a stock item.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - This only works if the barcode is not already associated with an object in the database
 | 
					    - This only works if the barcode is not already associated with an object in the database
 | 
				
			||||||
    - If the barcode does not match an object, then the barcode hash is assigned to the StockItem
 | 
					    - If the barcode does not match an object, then the barcode hash is assigned to the StockItem
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    permission_classes = [
 | 
					    serializer_class = barcode_serializers.BarcodeAssignSerializer
 | 
				
			||||||
        permissions.IsAuthenticated
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def handle_barcode(self, barcode: str, request, **kwargs):
 | 
				
			||||||
        """Respond to a barcode assign POST request.
 | 
					        """Respond to a barcode assign request.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Checks inputs and assign barcode (hash) to StockItem.
 | 
					        Checks inputs and assign barcode (hash) to StockItem.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        data = request.data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        barcode_data = data.get('barcode', None)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not barcode_data:
 | 
					 | 
				
			||||||
            raise ValidationError({'barcode': _('Missing barcode data')})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Here we only check against 'InvenTree' plugins
 | 
					        # Here we only check against 'InvenTree' plugins
 | 
				
			||||||
        plugins = registry.with_mixin('barcode', builtin=True)
 | 
					        plugins = registry.with_mixin('barcode', builtin=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # First check if the provided barcode matches an existing database entry
 | 
					        # First check if the provided barcode matches an existing database entry
 | 
				
			||||||
        for plugin in plugins:
 | 
					        for plugin in plugins:
 | 
				
			||||||
            result = plugin.scan(barcode_data)
 | 
					            result = plugin.scan(barcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if result is not None:
 | 
					            if result is not None:
 | 
				
			||||||
                result["error"] = _("Barcode matches existing item")
 | 
					                result["error"] = _("Barcode matches existing item")
 | 
				
			||||||
                result["plugin"] = plugin.name
 | 
					                result["plugin"] = plugin.name
 | 
				
			||||||
                result["barcode_data"] = barcode_data
 | 
					                result["barcode_data"] = barcode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                raise ValidationError(result)
 | 
					                raise ValidationError(result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        barcode_hash = hash_barcode(barcode_data)
 | 
					        barcode_hash = hash_barcode(barcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        valid_labels = []
 | 
					        valid_labels = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -144,39 +157,32 @@ class BarcodeAssign(APIView):
 | 
				
			|||||||
            label = model.barcode_model_type()
 | 
					            label = model.barcode_model_type()
 | 
				
			||||||
            valid_labels.append(label)
 | 
					            valid_labels.append(label)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if label in data:
 | 
					            if instance := kwargs.get(label, None):
 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    instance = model.objects.get(pk=data[label])
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    # Check that the user has the required permission
 | 
					                # Check that the user has the required permission
 | 
				
			||||||
                    app_label = model._meta.app_label
 | 
					                app_label = model._meta.app_label
 | 
				
			||||||
                    model_name = model._meta.model_name
 | 
					                model_name = model._meta.model_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    table = f"{app_label}_{model_name}"
 | 
					                table = f"{app_label}_{model_name}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if not RuleSet.check_table_permission(request.user, table, "change"):
 | 
					                if not RuleSet.check_table_permission(request.user, table, "change"):
 | 
				
			||||||
                        raise PermissionDenied({
 | 
					                    raise PermissionDenied({
 | 
				
			||||||
                            "error": f"You do not have the required permissions for {table}"
 | 
					                        "error": f"You do not have the required permissions for {table}"
 | 
				
			||||||
                        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    instance.assign_barcode(
 | 
					 | 
				
			||||||
                        barcode_data=barcode_data,
 | 
					 | 
				
			||||||
                        barcode_hash=barcode_hash,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    return Response({
 | 
					 | 
				
			||||||
                        'success': f"Assigned barcode to {label} instance",
 | 
					 | 
				
			||||||
                        label: {
 | 
					 | 
				
			||||||
                            'pk': instance.pk,
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                        "barcode_data": barcode_data,
 | 
					 | 
				
			||||||
                        "barcode_hash": barcode_hash,
 | 
					 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                except (ValueError, model.DoesNotExist):
 | 
					                instance.assign_barcode(
 | 
				
			||||||
                    raise ValidationError({
 | 
					                    barcode_data=barcode,
 | 
				
			||||||
                        'error': f"No matching {label} instance found in database",
 | 
					                    barcode_hash=barcode_hash,
 | 
				
			||||||
                    })
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return Response({
 | 
				
			||||||
 | 
					                    'success': f"Assigned barcode to {label} instance",
 | 
				
			||||||
 | 
					                    label: {
 | 
				
			||||||
 | 
					                        'pk': instance.pk,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    "barcode_data": barcode,
 | 
				
			||||||
 | 
					                    "barcode_hash": barcode_hash,
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # If we got here, it means that no valid model types were provided
 | 
					        # If we got here, it means that no valid model types were provided
 | 
				
			||||||
        raise ValidationError({
 | 
					        raise ValidationError({
 | 
				
			||||||
@@ -184,23 +190,23 @@ class BarcodeAssign(APIView):
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BarcodeUnassign(APIView):
 | 
					class BarcodeUnassign(BarcodeView):
 | 
				
			||||||
    """Endpoint for unlinking / unassigning a custom barcode from a database object"""
 | 
					    """Endpoint for unlinking / unassigning a custom barcode from a database object"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    permission_classes = [
 | 
					    serializer_class = barcode_serializers.BarcodeUnassignSerializer
 | 
				
			||||||
        permissions.IsAuthenticated,
 | 
					
 | 
				
			||||||
    ]
 | 
					    def create(self, request, *args, **kwargs):
 | 
				
			||||||
 | 
					        """Respond to a barcode unassign request."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = self.get_serializer(data=request.data)
 | 
				
			||||||
 | 
					        serializer.is_valid(raise_exception=True)
 | 
				
			||||||
 | 
					        data = serializer.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					 | 
				
			||||||
        """Respond to a barcode unassign POST request"""
 | 
					 | 
				
			||||||
        # The following database models support assignment of third-party barcodes
 | 
					 | 
				
			||||||
        supported_models = InvenTreeInternalBarcodePlugin.get_supported_barcode_models()
 | 
					        supported_models = InvenTreeInternalBarcodePlugin.get_supported_barcode_models()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        supported_labels = [model.barcode_model_type() for model in supported_models]
 | 
					        supported_labels = [model.barcode_model_type() for model in supported_models]
 | 
				
			||||||
        model_names = ', '.join(supported_labels)
 | 
					        model_names = ', '.join(supported_labels)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = request.data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        matched_labels = []
 | 
					        matched_labels = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for label in supported_labels:
 | 
					        for label in supported_labels:
 | 
				
			||||||
@@ -219,15 +225,10 @@ class BarcodeUnassign(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # At this stage, we know that we have received a single valid field
 | 
					        # At this stage, we know that we have received a single valid field
 | 
				
			||||||
        for model in supported_models:
 | 
					        for model in supported_models:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            label = model.barcode_model_type()
 | 
					            label = model.barcode_model_type()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if label in data:
 | 
					            if instance := data.get(label, None):
 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    instance = model.objects.get(pk=data[label])
 | 
					 | 
				
			||||||
                except (ValueError, model.DoesNotExist):
 | 
					 | 
				
			||||||
                    raise ValidationError({
 | 
					 | 
				
			||||||
                        label: _('No match found for provided value')
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # Check that the user has the required permission
 | 
					                # Check that the user has the required permission
 | 
				
			||||||
                app_label = model._meta.app_label
 | 
					                app_label = model._meta.app_label
 | 
				
			||||||
@@ -253,7 +254,7 @@ class BarcodeUnassign(APIView):
 | 
				
			|||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BarcodePOReceive(APIView):
 | 
					class BarcodePOReceive(BarcodeView):
 | 
				
			||||||
    """Endpoint for handling receiving parts by scanning their barcode.
 | 
					    """Endpoint for handling receiving parts by scanning their barcode.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Barcode data are decoded by the client application,
 | 
					    Barcode data are decoded by the client application,
 | 
				
			||||||
@@ -269,32 +270,16 @@ class BarcodePOReceive(APIView):
 | 
				
			|||||||
    - location: The destination location for the received item (optional)
 | 
					    - location: The destination location for the received item (optional)
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    permission_classes = [
 | 
					    serializer_class = barcode_serializers.BarcodePOReceiveSerializer
 | 
				
			||||||
        permissions.IsAuthenticated,
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def handle_barcode(self, barcode: str, request, **kwargs):
 | 
				
			||||||
        """Respond to a barcode POST request."""
 | 
					        """Handle a barcode scan for a purchase order item."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = request.data
 | 
					        logger.debug("BarcodePOReceive: scanned barcode - '%s'", barcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not (barcode_data := data.get("barcode")):
 | 
					        # Extract optional fields from the dataset
 | 
				
			||||||
            raise ValidationError({"barcode": _("Missing barcode data")})
 | 
					        purchase_order = kwargs.get('purchase_order', None)
 | 
				
			||||||
 | 
					        location = kwargs.get('location', None)
 | 
				
			||||||
        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:
 | 
					 | 
				
			||||||
                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")
 | 
					        plugins = registry.with_mixin("barcode")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -303,8 +288,10 @@ class BarcodePOReceive(APIView):
 | 
				
			|||||||
        response = {}
 | 
					        response = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        internal_barcode_plugin = next(filter(
 | 
					        internal_barcode_plugin = next(filter(
 | 
				
			||||||
            lambda plugin: plugin.name == "InvenTreeBarcode", plugins))
 | 
					            lambda plugin: plugin.name == "InvenTreeBarcode", plugins
 | 
				
			||||||
        if internal_barcode_plugin.scan(barcode_data):
 | 
					        ))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if internal_barcode_plugin.scan(barcode):
 | 
				
			||||||
            response["error"] = _("Item has already been received")
 | 
					            response["error"] = _("Item has already been received")
 | 
				
			||||||
            raise ValidationError(response)
 | 
					            raise ValidationError(response)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -314,7 +301,7 @@ class BarcodePOReceive(APIView):
 | 
				
			|||||||
        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,
 | 
				
			||||||
                request.user,
 | 
					                request.user,
 | 
				
			||||||
                purchase_order=purchase_order,
 | 
					                purchase_order=purchase_order,
 | 
				
			||||||
                location=location,
 | 
					                location=location,
 | 
				
			||||||
@@ -335,8 +322,8 @@ class BarcodePOReceive(APIView):
 | 
				
			|||||||
                break
 | 
					                break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response["plugin"] = plugin.name if plugin else None
 | 
					        response["plugin"] = plugin.name if plugin else None
 | 
				
			||||||
        response["barcode_data"] = barcode_data
 | 
					        response["barcode_data"] = barcode
 | 
				
			||||||
        response["barcode_hash"] = hash_barcode(barcode_data)
 | 
					        response["barcode_hash"] = hash_barcode(barcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # A plugin has not been found!
 | 
					        # A plugin has not been found!
 | 
				
			||||||
        if plugin is None:
 | 
					        if plugin is None:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										107
									
								
								InvenTree/plugin/base/barcodes/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								InvenTree/plugin/base/barcodes/serializers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					"""DRF serializers for barcode scanning API"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.core.exceptions import ValidationError
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rest_framework import serializers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import order.models
 | 
				
			||||||
 | 
					import stock.models
 | 
				
			||||||
 | 
					from InvenTree.status_codes import PurchaseOrderStatus
 | 
				
			||||||
 | 
					from plugin.builtin.barcodes.inventree_barcode import \
 | 
				
			||||||
 | 
					    InvenTreeInternalBarcodePlugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BarcodeSerializer(serializers.Serializer):
 | 
				
			||||||
 | 
					    """Generic serializer for receiving barcode data"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    MAX_BARCODE_LENGTH = 4095
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    barcode = serializers.CharField(
 | 
				
			||||||
 | 
					        required=True, help_text=_('Scanned barcode data'),
 | 
				
			||||||
 | 
					        max_length=MAX_BARCODE_LENGTH,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BarcodeAssignMixin(serializers.Serializer):
 | 
				
			||||||
 | 
					    """Serializer for linking and unlinking barcode to an internal class"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        """Generate serializer fields for each supported model type"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for model in InvenTreeInternalBarcodePlugin.get_supported_barcode_models():
 | 
				
			||||||
 | 
					            self.fields[model.barcode_model_type()] = serializers.PrimaryKeyRelatedField(
 | 
				
			||||||
 | 
					                queryset=model.objects.all(),
 | 
				
			||||||
 | 
					                required=False, allow_null=True,
 | 
				
			||||||
 | 
					                label=model._meta.verbose_name,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def get_model_fields():
 | 
				
			||||||
 | 
					        """Return a list of model fields"""
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            model.barcode_model_type() for model in InvenTreeInternalBarcodePlugin.get_supported_barcode_models()
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BarcodeAssignSerializer(BarcodeAssignMixin, BarcodeSerializer):
 | 
				
			||||||
 | 
					    """Serializer class for linking a barcode to an internal model"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        """Meta class for BarcodeAssignSerializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            'barcode',
 | 
				
			||||||
 | 
					            *BarcodeAssignMixin.get_model_fields()
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BarcodeUnassignSerializer(BarcodeAssignMixin):
 | 
				
			||||||
 | 
					    """Serializer class for unlinking a barcode from an internal model"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        """Meta class for BarcodeUnlinkSerializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fields = BarcodeAssignMixin.get_model_fields()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BarcodePOReceiveSerializer(BarcodeSerializer):
 | 
				
			||||||
 | 
					    """Serializer for receiving items against a purchase order.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The following additional fields may be specified:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - purchase_order: PurchaseOrder object to receive items against
 | 
				
			||||||
 | 
					    - location: Location to receive items into
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    purchase_order = serializers.PrimaryKeyRelatedField(
 | 
				
			||||||
 | 
					        queryset=order.models.PurchaseOrder.objects.all(),
 | 
				
			||||||
 | 
					        required=False,
 | 
				
			||||||
 | 
					        help_text=_('PurchaseOrder to receive items against'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate_purchase_order(self, order: order.models.PurchaseOrder):
 | 
				
			||||||
 | 
					        """Validate the provided order"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if order.status != PurchaseOrderStatus.PLACED.value:
 | 
				
			||||||
 | 
					            raise ValidationError(_("Purchase order has not been placed"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return order
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    location = serializers.PrimaryKeyRelatedField(
 | 
				
			||||||
 | 
					        queryset=stock.models.StockLocation.objects.all(),
 | 
				
			||||||
 | 
					        required=False,
 | 
				
			||||||
 | 
					        help_text=_('Location to receive items into'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate_location(self, location: stock.models.StockLocation):
 | 
				
			||||||
 | 
					        """Validate the provided location"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if location.structural:
 | 
				
			||||||
 | 
					            raise ValidationError(_("Cannot select a structural location"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return location
 | 
				
			||||||
@@ -2,9 +2,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from rest_framework import status
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from InvenTree.unit_test import InvenTreeAPITestCase
 | 
					from InvenTree.unit_test import InvenTreeAPITestCase
 | 
				
			||||||
 | 
					from part.models import Part
 | 
				
			||||||
from stock.models import StockItem
 | 
					from stock.models import StockItem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -24,150 +23,128 @@ class BarcodeAPITest(InvenTreeAPITestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.scan_url = reverse('api-barcode-scan')
 | 
					        self.scan_url = reverse('api-barcode-scan')
 | 
				
			||||||
        self.assign_url = reverse('api-barcode-link')
 | 
					        self.assign_url = reverse('api-barcode-link')
 | 
				
			||||||
 | 
					        self.unassign_url = reverse('api-barcode-unlink')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def postBarcode(self, url, barcode):
 | 
					    def postBarcode(self, url, barcode, expected_code=None):
 | 
				
			||||||
        """Post barcode and return results."""
 | 
					        """Post barcode and return results."""
 | 
				
			||||||
        return self.client.post(url, format='json', data={'barcode': str(barcode)})
 | 
					        return self.post(url, format='json', data={'barcode': str(barcode)}, expected_code=expected_code)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_invalid(self):
 | 
					    def test_invalid(self):
 | 
				
			||||||
        """Test that invalid requests fail."""
 | 
					        """Test that invalid requests fail."""
 | 
				
			||||||
        # test scan url
 | 
					        # test scan url
 | 
				
			||||||
        response = self.client.post(self.scan_url, format='json', data={})
 | 
					        self.post(self.scan_url, format='json', data={}, expected_code=400)
 | 
				
			||||||
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # test wrong assign urls
 | 
					        # test wrong assign urls
 | 
				
			||||||
        response = self.client.post(self.assign_url, format='json', data={})
 | 
					        self.post(self.assign_url, format='json', data={}, expected_code=400)
 | 
				
			||||||
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 | 
					        self.post(self.assign_url, format='json', data={'barcode': '123'}, expected_code=400)
 | 
				
			||||||
 | 
					        self.post(self.assign_url, format='json', data={'barcode': '123', 'stockitem': '123'}, expected_code=400)
 | 
				
			||||||
        response = self.client.post(self.assign_url, format='json', data={'barcode': '123'})
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        response = self.client.post(self.assign_url, format='json', data={'barcode': '123', 'stockitem': '123'})
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_empty(self):
 | 
					    def test_empty(self):
 | 
				
			||||||
        """Test an empty barcode scan.
 | 
					        """Test an empty barcode scan.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ensure that all required data is in the respomse.
 | 
					        Ensure that all required data is in the response.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        response = self.postBarcode(self.scan_url, '')
 | 
					        response = self.postBarcode(self.scan_url, '', expected_code=400)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, 400)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = response.data
 | 
					        data = response.data
 | 
				
			||||||
        self.assertIn('barcode', data)
 | 
					        self.assertIn('barcode', data)
 | 
				
			||||||
        self.assertIn('Missing barcode data', str(response.data['barcode']))
 | 
					
 | 
				
			||||||
 | 
					        self.assertIn('This field may not be blank', str(response.data['barcode']))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_find_part(self):
 | 
					    def test_find_part(self):
 | 
				
			||||||
        """Test that we can lookup a part based on ID."""
 | 
					        """Test that we can lookup a part based on ID."""
 | 
				
			||||||
        response = self.client.post(
 | 
					
 | 
				
			||||||
 | 
					        part = Part.objects.first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.post(
 | 
				
			||||||
            self.scan_url,
 | 
					            self.scan_url,
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                'barcode': {
 | 
					                'barcode': f'{{"part": {part.pk}}}',
 | 
				
			||||||
                    'part': 1,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            format='json',
 | 
					            expected_code=200
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(response.status_code, status.HTTP_200_OK)
 | 
					 | 
				
			||||||
        self.assertIn('part', response.data)
 | 
					        self.assertIn('part', response.data)
 | 
				
			||||||
        self.assertIn('barcode_data', response.data)
 | 
					        self.assertIn('barcode_data', response.data)
 | 
				
			||||||
        self.assertEqual(response.data['part']['pk'], 1)
 | 
					        self.assertEqual(response.data['part']['pk'], part.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_invalid_part(self):
 | 
					    def test_invalid_part(self):
 | 
				
			||||||
        """Test response for invalid part."""
 | 
					        """Test response for invalid part."""
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.post(
 | 
				
			||||||
            self.scan_url,
 | 
					            self.scan_url,
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                'barcode': {
 | 
					                'barcode': '{"part": 999999999}'
 | 
				
			||||||
                    'part': 999999999,
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            format='json'
 | 
					            expected_code=400
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(response.status_code, 400)
 | 
					 | 
				
			||||||
        self.assertIn('error', response.data)
 | 
					        self.assertIn('error', response.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_find_stock_item(self):
 | 
					    def test_find_stock_item(self):
 | 
				
			||||||
        """Test that we can lookup a stock item based on ID."""
 | 
					        """Test that we can lookup a stock item based on ID."""
 | 
				
			||||||
        response = self.client.post(
 | 
					
 | 
				
			||||||
 | 
					        item = StockItem.objects.first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.post(
 | 
				
			||||||
            self.scan_url,
 | 
					            self.scan_url,
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                'barcode': {
 | 
					                'barcode': item.format_barcode(),
 | 
				
			||||||
                    'stockitem': 1,
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            format='json',
 | 
					            expected_code=200
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(response.status_code, status.HTTP_200_OK)
 | 
					 | 
				
			||||||
        self.assertIn('stockitem', response.data)
 | 
					        self.assertIn('stockitem', response.data)
 | 
				
			||||||
        self.assertIn('barcode_data', response.data)
 | 
					        self.assertIn('barcode_data', response.data)
 | 
				
			||||||
        self.assertEqual(response.data['stockitem']['pk'], 1)
 | 
					        self.assertEqual(response.data['stockitem']['pk'], item.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_invalid_item(self):
 | 
					    def test_invalid_item(self):
 | 
				
			||||||
        """Test response for invalid stock item."""
 | 
					        """Test response for invalid stock item."""
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.post(
 | 
				
			||||||
            self.scan_url,
 | 
					            self.scan_url,
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                'barcode': {
 | 
					                'barcode': '{"stockitem": 999999999}'
 | 
				
			||||||
                    'stockitem': 999999999,
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            format='json'
 | 
					            expected_code=400
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(response.status_code, 400)
 | 
					 | 
				
			||||||
        self.assertIn('error', response.data)
 | 
					        self.assertIn('error', response.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_find_location(self):
 | 
					    def test_find_location(self):
 | 
				
			||||||
        """Test that we can lookup a stock location based on ID."""
 | 
					        """Test that we can lookup a stock location based on ID."""
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.post(
 | 
				
			||||||
            self.scan_url,
 | 
					            self.scan_url,
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                'barcode': {
 | 
					                'barcode': '{"stocklocation": 1}',
 | 
				
			||||||
                    'stocklocation': 1,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            format='json'
 | 
					            expected_code=200
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(response.status_code, status.HTTP_200_OK)
 | 
					 | 
				
			||||||
        self.assertIn('stocklocation', response.data)
 | 
					        self.assertIn('stocklocation', response.data)
 | 
				
			||||||
        self.assertIn('barcode_data', response.data)
 | 
					        self.assertIn('barcode_data', response.data)
 | 
				
			||||||
        self.assertEqual(response.data['stocklocation']['pk'], 1)
 | 
					        self.assertEqual(response.data['stocklocation']['pk'], 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_invalid_location(self):
 | 
					    def test_invalid_location(self):
 | 
				
			||||||
        """Test response for an invalid location."""
 | 
					        """Test response for an invalid location."""
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.post(
 | 
				
			||||||
            self.scan_url,
 | 
					            self.scan_url,
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                'barcode': {
 | 
					                'barcode': '{"stocklocation": 999999999}'
 | 
				
			||||||
                    'stocklocation': 999999999,
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            format='json'
 | 
					            expected_code=400
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(response.status_code, 400)
 | 
					 | 
				
			||||||
        self.assertIn('error', response.data)
 | 
					        self.assertIn('error', response.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_integer_barcode(self):
 | 
					    def test_integer_barcode(self):
 | 
				
			||||||
        """Test scan of an integer barcode."""
 | 
					        """Test scan of an integer barcode."""
 | 
				
			||||||
        response = self.postBarcode(self.scan_url, '123456789')
 | 
					        response = self.postBarcode(self.scan_url, '123456789', expected_code=400)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, 400)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = response.data
 | 
					        data = response.data
 | 
				
			||||||
        self.assertIn('error', data)
 | 
					        self.assertIn('error', data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_array_barcode(self):
 | 
					    def test_array_barcode(self):
 | 
				
			||||||
        """Test scan of barcode with string encoded array."""
 | 
					        """Test scan of barcode with string encoded array."""
 | 
				
			||||||
        response = self.postBarcode(self.scan_url, "['foo', 'bar']")
 | 
					        response = self.postBarcode(self.scan_url, "['foo', 'bar']", expected_code=400)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertEqual(response.status_code, 400)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = response.data
 | 
					        data = response.data
 | 
				
			||||||
        self.assertIn('error', data)
 | 
					        self.assertIn('error', data)
 | 
				
			||||||
@@ -176,11 +153,9 @@ class BarcodeAPITest(InvenTreeAPITestCase):
 | 
				
			|||||||
        """Test that a barcode is generated with a scan."""
 | 
					        """Test that a barcode is generated with a scan."""
 | 
				
			||||||
        item = StockItem.objects.get(pk=522)
 | 
					        item = StockItem.objects.get(pk=522)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.postBarcode(self.scan_url, item.format_barcode())
 | 
					        response = self.postBarcode(self.scan_url, item.format_barcode(), expected_code=200)
 | 
				
			||||||
        data = response.data
 | 
					        data = response.data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(response.status_code, status.HTTP_200_OK)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertIn('stockitem', data)
 | 
					        self.assertIn('stockitem', data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        pk = data['stockitem']['pk']
 | 
					        pk = data['stockitem']['pk']
 | 
				
			||||||
@@ -197,37 +172,88 @@ class BarcodeAPITest(InvenTreeAPITestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        barcode_data = 'A-TEST-BARCODE-STRING'
 | 
					        barcode_data = 'A-TEST-BARCODE-STRING'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.post(
 | 
				
			||||||
            self.assign_url, format='json',
 | 
					            self.assign_url, format='json',
 | 
				
			||||||
            data={
 | 
					            data={
 | 
				
			||||||
                'barcode': barcode_data,
 | 
					                'barcode': barcode_data,
 | 
				
			||||||
                'stockitem': item.pk
 | 
					                'stockitem': item.pk
 | 
				
			||||||
            }
 | 
					            },
 | 
				
			||||||
 | 
					            expected_code=200
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = response.data
 | 
					        data = response.data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(response.status_code, status.HTTP_200_OK)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertIn('success', data)
 | 
					        self.assertIn('success', data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        result_hash = data['barcode_hash']
 | 
					        result_hash = data['barcode_hash']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Read the item out from the database again
 | 
					        # Read the item out from the database again
 | 
				
			||||||
        item = StockItem.objects.get(pk=522)
 | 
					        item.refresh_from_db()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEqual(item.barcode_data, barcode_data)
 | 
				
			||||||
        self.assertEqual(result_hash, item.barcode_hash)
 | 
					        self.assertEqual(result_hash, item.barcode_hash)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Ensure that the same barcode hash cannot be assigned to a different stock item!
 | 
					        # Ensure that the same barcode hash cannot be assigned to a different stock item!
 | 
				
			||||||
        response = self.client.post(
 | 
					        response = self.post(
 | 
				
			||||||
            self.assign_url, format='json',
 | 
					            self.assign_url, format='json',
 | 
				
			||||||
            data={
 | 
					            data={
 | 
				
			||||||
                'barcode': barcode_data,
 | 
					                'barcode': barcode_data,
 | 
				
			||||||
                'stockitem': 521
 | 
					                'stockitem': 521
 | 
				
			||||||
            }
 | 
					            },
 | 
				
			||||||
 | 
					            expected_code=400
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data = response.data
 | 
					        self.assertIn('error', response.data)
 | 
				
			||||||
 | 
					        self.assertNotIn('success', response.data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertIn('error', data)
 | 
					        # Check that we can now unassign a barcode
 | 
				
			||||||
        self.assertNotIn('success', data)
 | 
					        response = self.post(
 | 
				
			||||||
 | 
					            self.unassign_url,
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'stockitem': item.pk,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            expected_code=200
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        item.refresh_from_db()
 | 
				
			||||||
 | 
					        self.assertEqual(item.barcode_data, '')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Check that the 'unassign' endpoint fails if the stockitem is invalid
 | 
				
			||||||
 | 
					        response = self.post(
 | 
				
			||||||
 | 
					            self.unassign_url,
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'stockitem': 999999999,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            expected_code=400
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_unassign_endpoint(self):
 | 
				
			||||||
 | 
					        """Test that the unassign endpoint works as expected"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        invalid_keys = ['cat', 'dog', 'fish']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Invalid key should fail
 | 
				
			||||||
 | 
					        for k in invalid_keys:
 | 
				
			||||||
 | 
					            response = self.post(
 | 
				
			||||||
 | 
					                self.unassign_url,
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    k: 123
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                expected_code=400
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.assertIn("Missing data: Provide one of", str(response.data['error']))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        valid_keys = ['build', 'salesorder', 'part']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Valid key but invalid pk should fail
 | 
				
			||||||
 | 
					        for k in valid_keys:
 | 
				
			||||||
 | 
					            response = self.post(
 | 
				
			||||||
 | 
					                self.unassign_url,
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    k: 999999999
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                expected_code=400
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.assertIn("object does not exist", str(response.data[k]))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,8 +63,6 @@ class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Here we are looking for a dict object which contains a reference to a particular InvenTree database object
 | 
					        Here we are looking for a dict object which contains a reference to a particular InvenTree database object
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # Create hash from raw barcode data
 | 
					 | 
				
			||||||
        barcode_hash = hash_barcode(barcode_data)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Attempt to coerce the barcode data into a dict object
 | 
					        # Attempt to coerce the barcode data into a dict object
 | 
				
			||||||
        # This is the internal barcode representation that InvenTree uses
 | 
					        # This is the internal barcode representation that InvenTree uses
 | 
				
			||||||
@@ -78,9 +76,11 @@ class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin):
 | 
				
			|||||||
            except json.JSONDecodeError:
 | 
					            except json.JSONDecodeError:
 | 
				
			||||||
                pass
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        supported_models = self.get_supported_barcode_models()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if barcode_dict is not None and type(barcode_dict) is dict:
 | 
					        if barcode_dict is not None and type(barcode_dict) is dict:
 | 
				
			||||||
            # Look for various matches. First good match will be returned
 | 
					            # Look for various matches. First good match will be returned
 | 
				
			||||||
            for model in self.get_supported_barcode_models():
 | 
					            for model in supported_models:
 | 
				
			||||||
                label = model.barcode_model_type()
 | 
					                label = model.barcode_model_type()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if label in barcode_dict:
 | 
					                if label in barcode_dict:
 | 
				
			||||||
@@ -91,8 +91,11 @@ class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin):
 | 
				
			|||||||
                    except (ValueError, model.DoesNotExist):
 | 
					                    except (ValueError, model.DoesNotExist):
 | 
				
			||||||
                        pass
 | 
					                        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Create hash from raw barcode data
 | 
				
			||||||
 | 
					        barcode_hash = hash_barcode(barcode_data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # If no "direct" hits are found, look for assigned third-party barcodes
 | 
					        # If no "direct" hits are found, look for assigned third-party barcodes
 | 
				
			||||||
        for model in self.get_supported_barcode_models():
 | 
					        for model in supported_models:
 | 
				
			||||||
            label = model.barcode_model_type()
 | 
					            label = model.barcode_model_type()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            instance = model.lookup_barcode(barcode_hash)
 | 
					            instance = model.lookup_barcode(barcode_hash)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -80,8 +80,8 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
 | 
				
			|||||||
        # Fail with too many fields provided
 | 
					        # Fail with too many fields provided
 | 
				
			||||||
        response = self.unassign(
 | 
					        response = self.unassign(
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                'stockitem': 'abcde',
 | 
					                'stockitem': stock.models.StockItem.objects.first().pk,
 | 
				
			||||||
                'part': 'abcde',
 | 
					                'part': part.models.Part.objects.first().pk,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            expected_code=400,
 | 
					            expected_code=400,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@@ -96,17 +96,17 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
 | 
				
			|||||||
            expected_code=400,
 | 
					            expected_code=400,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertIn('No match found', str(response.data['stockitem']))
 | 
					        self.assertIn('Incorrect type', str(response.data['stockitem']))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Fail with an invalid Part instance
 | 
					        # Fail with an invalid Part instance
 | 
				
			||||||
        response = self.unassign(
 | 
					        response = self.unassign(
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                'part': 'invalid',
 | 
					                'part': 99999999999,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            expected_code=400,
 | 
					            expected_code=400,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertIn('No match found', str(response.data['part']))
 | 
					        self.assertIn('object does not exist', str(response.data['part']))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_assign_to_stock_item(self):
 | 
					    def test_assign_to_stock_item(self):
 | 
				
			||||||
        """Test that we can assign a unique barcode to a StockItem object"""
 | 
					        """Test that we can assign a unique barcode to a StockItem object"""
 | 
				
			||||||
@@ -216,7 +216,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
 | 
				
			|||||||
            expected_code=400,
 | 
					            expected_code=400,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertIn('No matching part instance found in database', str(response.data))
 | 
					        self.assertIn('object does not exist', str(response.data['part']))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Test assigning to a valid part (should pass)
 | 
					        # Test assigning to a valid part (should pass)
 | 
				
			||||||
        response = self.assign(
 | 
					        response = self.assign(
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user