2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-07-04 06:00:38 +00:00

extend barcode scans API (#12233)

* extend barcode scans with user perm check

* fix import

* fix call

* align error message

* add missing permissions to test

* remove erronous assign

* ensure permission erros knock through
This commit is contained in:
Matthias Mair
2026-06-24 02:45:26 +02:00
committed by GitHub
parent 78a00d320a
commit f21bc2d06f
8 changed files with 48 additions and 17 deletions
+2 -2
View File
@@ -54,14 +54,14 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin):
VERSION = "0.0.1" VERSION = "0.0.1"
AUTHOR = "Michael" AUTHOR = "Michael"
def scan(self, barcode_data): def scan(self, barcode_data, user, **kwargs):
if barcode_data.startswith("PART-"): if barcode_data.startswith("PART-"):
try: try:
pk = int(barcode_data.split("PART-")[1]) pk = int(barcode_data.split("PART-")[1])
instance = Part.objects.get(pk=pk) instance = Part.objects.get(pk=pk)
label = Part.barcode_model_type() label = Part.barcode_model_type()
return {label: instance.format_matched_response()} return {label: instance.format_matched_response(user=user)}
except Part.DoesNotExist: except Part.DoesNotExist:
pass pass
``` ```
+10 -1
View File
@@ -23,6 +23,7 @@ from django_q.models import Task
from error_report.models import Error from error_report.models import Error
from mptt.exceptions import InvalidMove from mptt.exceptions import InvalidMove
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from rest_framework.exceptions import PermissionDenied
from stdimage.models import StdImageField from stdimage.models import StdImageField
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
@@ -1334,8 +1335,16 @@ class InvenTreeBarcodeMixin(models.Model):
return generate_barcode(self) return generate_barcode(self)
def format_matched_response(self): def format_matched_response(self, user, **kwargs):
"""Format a standard response for a matched barcode.""" """Format a standard response for a matched barcode."""
# Check permission for this object
from users.permissions import check_user_permission
if not check_user_permission(user, self, 'view'):
raise PermissionDenied(
_('User does not have permission to view this model')
)
data = {'pk': self.pk} data = {'pk': self.pk}
if hasattr(self, 'get_api_url'): if hasattr(self, 'get_api_url'):
@@ -153,7 +153,9 @@ class BarcodeView(CreateAPIView):
for current_plugin in plugins: for current_plugin in plugins:
try: try:
result = current_plugin.scan(barcode) result = current_plugin.scan(barcode, user=request.user, **kwargs)
except PermissionDenied as exc:
raise exc
except Exception: except Exception:
log_error('BarcodeView.scan_barcode', plugin=current_plugin.slug) log_error('BarcodeView.scan_barcode', plugin=current_plugin.slug)
continue continue
@@ -282,7 +284,7 @@ class BarcodeAssign(BarcodeView):
# First check if the provided barcode matches an existing database entry # First check if the provided barcode matches an existing database entry
if inventree_barcode_plugin: if inventree_barcode_plugin:
result = inventree_barcode_plugin.scan(barcode) result = inventree_barcode_plugin.scan(barcode, user=request.user, **kwargs)
if result is not None: if result is not None:
result['error'] = _('Barcode matches existing item') result['error'] = _('Barcode matches existing item')
@@ -459,7 +461,9 @@ class BarcodePOAllocate(BarcodeView):
manufacturer_part=response.get('manufacturerpart', None), manufacturer_part=response.get('manufacturerpart', None),
) )
response['success'] = _('Matched supplier part') response['success'] = _('Matched supplier part')
response['supplierpart'] = supplier_part.format_matched_response() response['supplierpart'] = supplier_part.format_matched_response(
user=request.user
)
except ValidationError as e: except ValidationError as e:
response['error'] = str(e) response['error'] = str(e)
@@ -524,7 +528,7 @@ class BarcodePOReceive(BarcodeView):
filter(lambda plugin: plugin.name == 'InvenTreeBarcode', plugins) filter(lambda plugin: plugin.name == 'InvenTreeBarcode', plugins)
) )
if result := internal_barcode_plugin.scan(barcode): if result := internal_barcode_plugin.scan(barcode, user=request.user, **kwargs):
if 'stockitem' in result: if 'stockitem' in result:
response['error'] = _('Item has already been received') response['error'] = _('Item has already been received')
self.log_scan(request, response, False) self.log_scan(request, response, False)
@@ -42,7 +42,7 @@ class BarcodeMixin:
"""Does this plugin have everything needed to process a barcode.""" """Does this plugin have everything needed to process a barcode."""
return True return True
def scan(self, barcode_data): def scan(self, barcode_data: str, user, **kwargs) -> dict | None:
"""Scan a barcode against this plugin. """Scan a barcode against this plugin.
This method is explicitly called from the /scan/ API endpoint, This method is explicitly called from the /scan/ API endpoint,
@@ -261,7 +261,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
'extract_barcode_fields must be implemented by each plugin' 'extract_barcode_fields must be implemented by each plugin'
) )
def scan(self, barcode_data: str) -> dict | None: def scan(self, barcode_data: str, user, **kwargs) -> dict | None:
"""Perform a generic 'scan' operation on a supplier barcode. """Perform a generic 'scan' operation on a supplier barcode.
The supplier barcode may provide sufficient information to match against The supplier barcode may provide sufficient information to match against
@@ -297,7 +297,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
for k, v in matches.items(): for k, v in matches.items():
if v and hasattr(v, 'pk'): if v and hasattr(v, 'pk'):
has_match = True has_match = True
data[k] = v.format_matched_response() data[k] = v.format_matched_response(user=user)
if not has_match: if not has_match:
return None return None
@@ -15,6 +15,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
"""Tests for barcode api.""" """Tests for barcode api."""
fixtures = ['category', 'part', 'location', 'stock'] fixtures = ['category', 'part', 'location', 'stock']
roles = ['stock.view', 'stock_location.view', 'part.view']
def setUp(self): def setUp(self):
"""Setup for all tests.""" """Setup for all tests."""
@@ -259,6 +260,7 @@ class SOAllocateTest(InvenTreeAPITestCase):
"""Unit tests for the barcode endpoint for allocating items to a sales order.""" """Unit tests for the barcode endpoint for allocating items to a sales order."""
fixtures = ['category', 'company', 'part', 'location', 'stock'] fixtures = ['category', 'company', 'part', 'location', 'stock']
roles = ['stock.view']
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -343,10 +345,14 @@ class SOAllocateTest(InvenTreeAPITestCase):
# Test with barcode which points to a *part* instance # Test with barcode which points to a *part* instance
item.part.assign_barcode(barcode_data='abcde') item.part.assign_barcode(barcode_data='abcde')
# missing permission for viewing the part - error
self.postBarcode('abcde', sales_order=self.sales_order.pk, expected_code=403)
# Add part.view role and test again
self.assignRole('part.view')
result = self.postBarcode( result = self.postBarcode(
'abcde', sales_order=self.sales_order.pk, expected_code=400 'abcde', sales_order=self.sales_order.pk, expected_code=400
) )
self.assertIn('does not match an existing stock item', str(result['error'])) self.assertIn('does not match an existing stock item', str(result['error']))
def test_submit(self): def test_submit(self):
@@ -48,11 +48,11 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi
}, },
} }
def format_matched_response(self, label, model, instance): def format_matched_response(self, label, model, instance, user, **kwargs):
"""Format a response for the scanned data.""" """Format a response for the scanned data."""
return {label: instance.format_matched_response()} return {label: instance.format_matched_response(user=user, **kwargs)}
def scan(self, barcode_data): def scan(self, barcode_data, user, **kwargs):
"""Scan a barcode against this plugin. """Scan a barcode against this plugin.
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
@@ -79,7 +79,7 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi
try: try:
instance = model.objects.get(pk=int(pk)) instance = model.objects.get(pk=int(pk))
return self.format_matched_response(label, model, instance) return self.format_matched_response(label, model, instance, user=user)
except (ValueError, model.DoesNotExist): except (ValueError, model.DoesNotExist):
pass pass
@@ -111,7 +111,9 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi
instance = model.objects.get(pk=pk) instance = model.objects.get(pk=pk)
return { return {
**self.format_matched_response(label, model, instance), **self.format_matched_response(
label, model, instance, user=user
),
'success': succcess_message, 'success': succcess_message,
} }
except (ValueError, model.DoesNotExist): except (ValueError, model.DoesNotExist):
@@ -129,7 +131,7 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi
if instance is not None: if instance is not None:
return { return {
**self.format_matched_response(label, model, instance), **self.format_matched_response(label, model, instance, user=user),
'success': succcess_message, 'success': succcess_message,
} }
@@ -11,6 +11,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
"""Tests for the integrated InvenTreeBarcode barcode plugin.""" """Tests for the integrated InvenTreeBarcode barcode plugin."""
fixtures = ['category', 'part', 'location', 'stock', 'company', 'supplier_part'] fixtures = ['category', 'part', 'location', 'stock', 'company', 'supplier_part']
roles = ['stock.view', 'stock_location.view', 'part.view']
def setUp(self): def setUp(self):
"""Set up the test case.""" """Set up the test case."""
@@ -14,6 +14,13 @@ class SupplierBarcodeTests(InvenTreeAPITestCase):
"""Tests barcode parsing for all suppliers.""" """Tests barcode parsing for all suppliers."""
SCAN_URL = reverse('api-barcode-scan') SCAN_URL = reverse('api-barcode-scan')
roles = [
'stock.view',
'stock_location.view',
'part.view',
'company.view',
'order.view',
]
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@@ -176,6 +183,8 @@ class SupplierBarcodeTests(InvenTreeAPITestCase):
class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase): class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
"""Tests barcode scanning to receive a purchase order item.""" """Tests barcode scanning to receive a purchase order item."""
roles = ['stock.view', 'stock_location.view']
def setUp(self): def setUp(self):
"""Create supplier part and purchase_order.""" """Create supplier part and purchase_order."""
super().setUp() super().setUp()