2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 19:46:46 +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:
Oliver 2023-11-20 12:51:49 +11:00 committed by GitHub
parent 8cb2ed3bd6
commit 52b01b09bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 338 additions and 211 deletions

View File

@ -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():

View File

@ -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)]

View File

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

View File

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

View 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

View File

@ -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]))

View File

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

View File

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