diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py new file mode 100644 index 0000000000..d68ecd67ad --- /dev/null +++ b/InvenTree/InvenTree/api.py @@ -0,0 +1,142 @@ +""" +Main JSON interface views +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.utils.translation import ugettext as _ +from django.http import JsonResponse + +from rest_framework import permissions +from rest_framework.response import Response +from rest_framework.views import APIView + +from .views import AjaxView +from .version import inventreeVersion, inventreeInstanceName + +from plugins import plugins as inventree_plugins + +# Load barcode plugins +print("INFO: Loading plugins") + +barcode_plugins = inventree_plugins.load_barcode_plugins() +action_plugins = inventree_plugins.load_action_plugins() + + +class InfoView(AjaxView): + """ Simple JSON endpoint for InvenTree information. + Use to confirm that the server is running, etc. + """ + + def get(self, request, *args, **kwargs): + + data = { + 'server': 'InvenTree', + 'version': inventreeVersion(), + 'instance': inventreeInstanceName(), + } + + return JsonResponse(data) + + +class ActionPluginView(APIView): + """ + Endpoint for running custom action plugins. + """ + + permission_classes = [ + permissions.IsAuthenticated, + ] + + def post(self, request, *args, **kwargs): + + action = request.data.get('action', None) + + data = request.data.get('data', None) + + if action is None: + return Response({ + 'error': _("No action specified") + }) + + for plugin_class in action_plugins: + if plugin_class.action_name() == action: + + plugin = plugin_class(request.user, data=data) + + plugin.perform_action() + + return Response(plugin.get_response()) + + # If we got to here, no matching action was found + return Response({ + 'error': _("No matching action found"), + "action": action, + }) + + +class BarcodePluginView(APIView): + """ + Endpoint for handling barcode scan requests. + + Barcode data are decoded by the client application, + and sent to this endpoint (as a JSON object) for validation. + + A barcode could follow the internal InvenTree barcode format, + or it could match to a third-party barcode format (e.g. Digikey). + + """ + + permission_classes = [ + permissions.IsAuthenticated, + ] + + def post(self, request, *args, **kwargs): + + response = {} + + barcode_data = request.data.get('barcode', None) + + print("Barcode data:") + print(barcode_data) + + if barcode_data is None: + response['error'] = _('No barcode data provided') + else: + # Look for a barcode plugin that knows how to handle the data + for plugin_class in barcode_plugins: + + # Instantiate the plugin with the provided plugin data + plugin = plugin_class(barcode_data) + + if plugin.validate(): + + # Plugin should return a dict response + response = plugin.decode() + + if type(response) is dict: + if 'success' not in response.keys() and 'error' not in response.keys(): + response['success'] = _('Barcode successfully decoded') + else: + response = { + 'error': _('Barcode plugin returned incorrect response') + } + + response['plugin'] = plugin.plugin_name() + response['hash'] = plugin.hash() + + break + + if 'error' not in response and 'success' not in response: + response = { + 'error': _('Unknown barcode format'), + } + + # Include the original barcode data + response['barcode_data'] = barcode_data + + print("Response:") + print(response) + + return Response(response) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 6b619b4aa2..b9a4d73740 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -172,7 +172,7 @@ def WrapWithQuotes(text, quote='"'): return text -def MakeBarcode(object_type, object_id, object_url, data={}): +def MakeBarcode(object_name, object_data): """ Generate a string for a barcode. Adds some global InvenTree parameters. Args: @@ -185,13 +185,12 @@ def MakeBarcode(object_type, object_id, object_url, data={}): json string of the supplied data plus some other data """ - # Add in some generic InvenTree data - data['type'] = object_type - data['id'] = object_id - data['url'] = object_url - data['tool'] = 'InvenTree' - data['instance'] = inventreeInstanceName() - data['version'] = inventreeVersion() + data = { + 'tool': 'InvenTree', + 'version': inventreeVersion(), + 'instance': inventreeInstanceName(), + object_name: object_data + } return json.dumps(data, sort_keys=True) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 0bb36db59f..5b13663897 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -27,6 +27,22 @@ class APITests(APITestCase): User = get_user_model() User.objects.create_user(self.username, 'user@email.com', self.password) + def test_info_view(self): + """ + Test that we can read the 'info-view' endpoint. + """ + + url = reverse('api-inventree-info') + + response = self.client.get(url, format='json') + + data = response.json() + self.assertIn('server', data) + self.assertIn('version', data) + self.assertIn('instance', data) + + self.assertEquals('InvenTree', data['server']) + def test_get_token_fail(self): """ Ensure that an invalid user cannot get a token """ @@ -65,3 +81,7 @@ class APITests(APITestCase): response = self.client.get(part_url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_barcode(self): + # TODO - Complete this + pass diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index d93a40e631..203748de3e 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -113,15 +113,15 @@ class TestMakeBarcode(TestCase): def test_barcode(self): - data = { - 'animal': 'cat', - 'legs': 3, - 'noise': 'purr' - } + bc = helpers.MakeBarcode( + "part", + { + "id": 3, + "url": "www.google.com", + } + ) - bc = helpers.MakeBarcode("part", 3, "www.google.com", data) - - self.assertIn('animal', bc) + self.assertIn('part', bc) self.assertIn('tool', bc) self.assertIn('"tool": "InvenTree"', bc) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index fd36fa9112..d9600333f4 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -35,7 +35,8 @@ from rest_framework.documentation import include_docs_urls from .views import IndexView, SearchView, DatabaseStatsView from .views import SettingsView, EditUserView, SetPasswordView -from .views import InfoView + +from .api import InfoView, BarcodePluginView, ActionPluginView from users.urls import user_urls @@ -53,8 +54,12 @@ apipatterns = [ # User URLs url(r'^user/', include(user_urls)), + # Plugin endpoints + url(r'^barcode/', BarcodePluginView.as_view(), name='api-barcode-plugin'), + url(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'), + # InvenTree information endpoint - url(r'^$', InfoView.as_view(), name='inventree-info'), + url(r'^$', InfoView.as_view(), name='api-inventree-info'), ] settings_urls = [ diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 59833d3e6b..943a18d35c 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -22,7 +22,6 @@ from common.models import InvenTreeSetting from .forms import DeleteForm, EditUserForm, SetPasswordForm from .helpers import str2bool -from .version import inventreeVersion, inventreeInstanceName from rest_framework import views @@ -416,22 +415,6 @@ class AjaxDeleteView(AjaxMixin, UpdateView): return self.renderJsonResponse(request, form, data=data, context=context) -class InfoView(AjaxView): - """ Simple JSON endpoint for InvenTree information. - Use to confirm that the server is running, etc. - """ - - def get(self, request, *args, **kwargs): - - data = { - 'server': 'InvenTree', - 'version': inventreeVersion(), - 'instance': inventreeInstanceName(), - } - - return JsonResponse(data) - - class EditUserView(AjaxUpdateView): """ View for editing user information """ diff --git a/InvenTree/company/templates/company/supplier_part_base.html b/InvenTree/company/templates/company/supplier_part_base.html index 964c61ee5e..fec430628b 100644 --- a/InvenTree/company/templates/company/supplier_part_base.html +++ b/InvenTree/company/templates/company/supplier_part_base.html @@ -61,7 +61,7 @@ InvenTree | {% trans "Supplier Part" %}
- | {% trans "Supplier" %} | -{{ order.supplier }} | ++ | {% trans "Order Reference" %} | +{{ order.reference }} |
- | {% trans "Status" %} | +{% trans "Order Status" %} | {% order_status order.status %} | ||
+ | {% trans "Supplier" %} | +{{ order.supplier.name }} | +|||
+ | {% trans "Supplier Reference" %} | +{{ order.supplier_reference }} | +|||
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index c182cc6583..ca5b8f11c2 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -478,11 +478,11 @@ class Part(models.Model): """ Return a JSON string for formatting a barcode for this Part object """ return helpers.MakeBarcode( - "Part", - self.id, - reverse('api-part-detail', kwargs={'pk': self.id}), + "part", { - 'name': self.name, + "id": self.id, + "name": self.full_name, + "url": reverse('api-part-detail', kwargs={'pk': self.id}), } ) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index d5270cabb2..788613e104 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -96,6 +96,8 @@ class PartSerializer(InvenTreeModelSerializer): queryset = queryset.prefetch_related('builds') return queryset + # TODO - Include a 'category_detail' field which serializers the category object + class Meta: model = Part partial = True diff --git a/InvenTree/plugins/__init__.py b/InvenTree/plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugins/action/__init__.py b/InvenTree/plugins/action/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugins/action/action.py b/InvenTree/plugins/action/action.py new file mode 100644 index 0000000000..4e0b0f5cb0 --- /dev/null +++ b/InvenTree/plugins/action/action.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +import plugins.plugin as plugin + + +class ActionPlugin(plugin.InvenTreePlugin): + """ + The ActionPlugin class is used to perform custom actions + """ + + ACTION_NAME = "" + + @classmethod + def action_name(cls): + """ + Return the action name for this plugin. + If the ACTION_NAME parameter is empty, + look at the PLUGIN_NAME instead. + """ + action = cls.ACTION_NAME + + if not action: + action = cls.PLUGIN_NAME + + return action + + def __init__(self, user, data=None): + """ + An action plugin takes a user reference, and an optional dataset (dict) + """ + plugin.InvenTreePlugin.__init__(self) + + self.user = user + self.data = data + + def perform_action(self): + """ + Override this method to perform the action! + """ + pass + + def get_result(self): + """ + Result of the action? + """ + + # Re-implement this for cutsom actions + return False + + def get_info(self): + """ + Extra info? Can be a string / dict / etc + """ + return None + + def get_response(self): + """ + Return a response. Default implementation is a simple response + which can be overridden. + """ + return { + "action": self.action_name(), + "result": self.get_result(), + "info": self.get_info(), + } + + +class SimpleActionPlugin(ActionPlugin): + """ + An EXTREMELY simple action plugin which demonstrates + the capability of the ActionPlugin class + """ + + PLUGIN_NAME = "SimpleActionPlugin" + ACTION_NAME = "simple" + + def perform_action(self): + print("Action plugin in action!") + + def get_info(self): + return { + "user": self.user.username, + "hello": "world", + } + + def get_result(self): + return True diff --git a/InvenTree/plugins/barcode/__init__.py b/InvenTree/plugins/barcode/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/InvenTree/plugins/barcode/barcode.py b/InvenTree/plugins/barcode/barcode.py new file mode 100644 index 0000000000..f8bd82f744 --- /dev/null +++ b/InvenTree/plugins/barcode/barcode.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +import hashlib + +from rest_framework.renderers import JSONRenderer + +from stock.serializers import StockItemSerializer, LocationSerializer +from part.serializers import PartSerializer + +import plugins.plugin as plugin + + +class BarcodePlugin(plugin.InvenTreePlugin): + """ + The BarcodePlugin class is the base class for any barcode plugin. + """ + + def __init__(self, barcode_data): + plugin.InvenTreePlugin.__init__(self) + + self.data = barcode_data + + def hash(self): + """ + Calculate a hash for the barcode data. + This is supposed to uniquely identify the barcode contents, + at least within the bardcode sub-type. + + The default implementation simply returns an MD5 hash of the barcode data, + encoded to a string. + + This may be sufficient for most applications, but can obviously be overridden + by a subclass. + + """ + + hash = hashlib.md5(str(self.data).encode()) + return str(hash.hexdigest()) + + def validate(self): + """ + Default implementation returns False + """ + return False + + def decode(self): + """ + Decode the barcode, and craft a response + """ + + return None + + def render_part(self, part): + """ + Render a Part object to JSON + Use the existing serializer to do this. + """ + + serializer = PartSerializer(part) + + return serializer.data + + def render_stock_location(self, loc): + """ + Render a StockLocation object to JSON + Use the existing serializer to do this. + """ + + serializer = LocationSerializer(loc) + + return serializer.data + + def render_stock_item(self, item): + """ + Render a StockItem object to JSON. + Use the existing serializer to do this + """ + + serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_detail=True) + + return serializer.data diff --git a/InvenTree/plugins/barcode/digikey.py b/InvenTree/plugins/barcode/digikey.py new file mode 100644 index 0000000000..2542fe964a --- /dev/null +++ b/InvenTree/plugins/barcode/digikey.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from . import barcode + + +class DigikeyBarcodePlugin(barcode.BarcodePlugin): + + PLUGIN_NAME = "DigikeyBarcodePlugin" diff --git a/InvenTree/plugins/barcode/inventree.py b/InvenTree/plugins/barcode/inventree.py new file mode 100644 index 0000000000..93b86d42b7 --- /dev/null +++ b/InvenTree/plugins/barcode/inventree.py @@ -0,0 +1,94 @@ +""" +The InvenTreeBarcodePlugin validates barcodes generated by InvenTree itself. +It can be used as a template for developing third-party barcode plugins. + +The data format is very simple, and maps directly to database objects, +via the "id" parameter. + +Parsing an InvenTree barcode simply involves validating that the +references model objects actually exist in the database. +""" + +# -*- coding: utf-8 -*- + +import json + +from . import barcode + +from stock.models import StockItem, StockLocation +from part.models import Part + +from django.utils.translation import ugettext as _ + + +class InvenTreeBarcodePlugin(barcode.BarcodePlugin): + + PLUGIN_NAME = "InvenTreeBarcodePlugin" + + def validate(self): + """ + An "InvenTree" barcode must include the following tags: + + { + 'tool': 'InvenTree', + 'version': | |||||
+ | {% trans "Belongs To" %} | {{ item.belongs_to }} | {{ item.location.name }} | {% endif %} + {% if item.uid %} +||
+ | {% trans "Unique Identifier" %} | +{{ item.uid }} | +|||