diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 171fe414d2..368f414da6 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -13,13 +13,18 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters from rest_framework import permissions +from rest_framework.exceptions import ParseError, NotFound from rest_framework.response import Response from rest_framework.views import APIView +from InvenTree.tasks import offload_task + from .views import AjaxView from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName from .status import is_worker_running +from stock.models import StockItem, StockLocation + from plugin import registry @@ -114,7 +119,75 @@ class ActionPluginView(APIView): return Response(plugin.get_response()) # If we got to here, no matching action was found - return Response({ + raise NotFound({ 'error': _("No matching action found"), "action": action, }) + + +class LocatePluginView(APIView): + """ + Endpoint for using a custom plugin to identify or 'locate' a stock item or location + """ + + permission_classes = [ + permissions.IsAuthenticated, + ] + + def post(self, request, *args, **kwargs): + + # Which plugin to we wish to use? + plugin = request.data.get('plugin', None) + + if not plugin: + raise ParseError("'plugin' field must be supplied") + + # Check that the plugin exists, and supports the 'locate' mixin + plugins = registry.with_mixin('locate') + + if plugin not in [p.slug for p in plugins]: + raise ParseError(f"Plugin '{plugin}' is not installed, or does not support the location mixin") + + # StockItem to identify + item_pk= request.data.get('item', None) + + # StockLocation to identify + location_pk = request.data.get('location', None) + + if not item_pk and not location_pk: + raise ParseError("Must supply either 'item' or 'location' parameter") + + data = { + "success": "Identification plugin activated", + "plugin": plugin, + } + + # StockItem takes priority + if item_pk: + try: + item = StockItem.objects.get(pk=item_pk) + + offload_task('plugin.registry.call_function', plugin, 'locate_stock_item', item_pk) + + data['item'] = item_pk + + return Response(data) + + except StockItem.DoesNotExist: + raise NotFound("StockItem matching PK '{item}' not found") + + elif location_pk: + try: + location = StockItem.objects.get(pk=location_pk) + + offload_task('plugin.registry.call_function', plugin, 'locate_stock_location', location_pk) + + data['location'] = location_pk + + return Response(data) + + except StockLocation.DoesNotExist: + raise NotFound("StockLocation matching PK {'location'} not found") + + else: + raise NotFound() diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 2b31d7c3b5..62524be89e 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -45,7 +45,7 @@ from .views import DynamicJsView from .views import NotificationsView from .api import InfoView, NotFoundView -from .api import ActionPluginView +from .api import ActionPluginView, LocatePluginView from users.api import user_urls @@ -75,6 +75,7 @@ apipatterns += [ # Plugin endpoints re_path(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'), + re_path(r'^locate/', LocatePluginView.as_view(), name='api-locate-plugin'), # Webhook enpoint path('', include(common_api_urls)), diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 84ed88c388..fc8bcce296 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -473,6 +473,8 @@ class LocateMixin: Note: A custom implemenation could always change this behaviour """ + logger.info(f"LocateMixin: Attempting to locate StockItem pk={item_pk}") + from stock.models import StockItem try: @@ -482,6 +484,7 @@ class LocateMixin: self.locate_stock_location(item.location.pk) except StockItem.DoesNotExist: + logger.warning("LocateMixin: StockItem pk={item_pk} not found") pass def locate_stock_location(self, location_pk): diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 2d7877acf4..cd0f8f00de 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -520,6 +520,14 @@ $("#barcode-scan-into-location").click(function() { }); }); +{% if plugins_enabled %} +$('#locate-item-button').click(function() { + locateItemOrLocation({ + item: {{ item.pk }}, + }); +}); +{% endif %} + function itemAdjust(action) { inventreeGet( diff --git a/InvenTree/templates/js/translated/plugin.js b/InvenTree/templates/js/translated/plugin.js index c612dd1e8c..cac44222be 100644 --- a/InvenTree/templates/js/translated/plugin.js +++ b/InvenTree/templates/js/translated/plugin.js @@ -7,6 +7,7 @@ /* exported installPlugin, + locateItemOrLocation */ function installPlugin() { @@ -24,3 +25,50 @@ function installPlugin() { } }); } + + +function locateItemOrLocation(options={}) { + + if (!options.item && !options.location) { + console.error("locateItemOrLocation: Either 'item' or 'location' must be provided!"); + return; + } + + function performLocate(plugin) { + inventreePut( + '{% url "api-locate-plugin" %}', + { + plugin: plugin, + item: options.item, + location: options.location, + }, + { + method: 'POST', + }, + ); + } + + // Request the list of available 'locate' plugins + inventreeGet( + '{% url "api-plugin-list" %}', + { + mixin: 'locate', + }, + { + success: function(plugins) { + // No 'locate' plugins are available! + if (plugins.length == 0) { + console.warn("No 'locate' plugins are available"); + } else if (plugins.length == 1) { + // Only a single locate plugin is available + performLocate(plugins[0].key); + } else { + // More than 1 location plugin available + // Select from a list + } + } + }, + ); +} + +