From 9ff5032020a3c07996386e0139b6c455ed1f28af Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 13 Apr 2020 23:39:56 +1000 Subject: [PATCH 01/23] Create simple endpoint for barcode decode --- InvenTree/InvenTree/api.py | 41 +++++++++++++++++++++++++++++++++ InvenTree/InvenTree/test_api.py | 16 +++++++++++++ InvenTree/InvenTree/urls.py | 8 +++++-- InvenTree/InvenTree/views.py | 17 -------------- 4 files changed, 63 insertions(+), 19 deletions(-) create mode 100644 InvenTree/InvenTree/api.py diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py new file mode 100644 index 0000000000..e242f07e0f --- /dev/null +++ b/InvenTree/InvenTree/api.py @@ -0,0 +1,41 @@ +""" +Main JSON interface views +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.http import JsonResponse + +from .views import AjaxView +from .version import inventreeVersion, inventreeInstanceName + + +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 BarcodeScanView(AjaxView): + """ + Endpoint for handling barcode scan requests. + """ + + def get(self, request, *args, **kwargs): + + data = { + 'barcode': 'Hello world', + } + + return JsonResponse(data) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 0bb36db59f..0851815cfd 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -27,6 +27,18 @@ 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') + + print(response) + print(dir(response)) + def test_get_token_fail(self): """ Ensure that an invalid user cannot get a token """ @@ -65,3 +77,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): + + url = reverse('api-barcode-view') \ No newline at end of file diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index fd36fa9112..1d1fabc795 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, BarcodeScanView from users.urls import user_urls @@ -53,8 +54,11 @@ apipatterns = [ # User URLs url(r'^user/', include(user_urls)), + # Barcode scanning endpoint + url(r'^barcode/', BarcodeScanView.as_view(), name='api-barcode-scan'), + # 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 """ From b286a5e30cbf392ec79d955ac86c0cac12010fae Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 01:17:44 +1000 Subject: [PATCH 02/23] Plugin framework - Registers some very simple barcode plugins --- InvenTree/plugins/__init__.py | 0 InvenTree/plugins/barcode/__init__.py | 0 InvenTree/plugins/barcode/barcode.py | 19 ++++++++++ InvenTree/plugins/barcode/digikey.py | 8 ++++ InvenTree/plugins/barcode/inventree.py | 14 +++++++ InvenTree/plugins/plugins.py | 52 ++++++++++++++++++++++++++ 6 files changed, 93 insertions(+) create mode 100644 InvenTree/plugins/__init__.py create mode 100644 InvenTree/plugins/barcode/__init__.py create mode 100644 InvenTree/plugins/barcode/barcode.py create mode 100644 InvenTree/plugins/barcode/digikey.py create mode 100644 InvenTree/plugins/barcode/inventree.py create mode 100644 InvenTree/plugins/plugins.py diff --git a/InvenTree/plugins/__init__.py b/InvenTree/plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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..a2d3c6652e --- /dev/null +++ b/InvenTree/plugins/barcode/barcode.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + + +class BarcodePlugin: + """ + The BarcodePlugin class is the base class for any barcode plugin. + """ + + # Override this for each actual plugin + PLUGIN_NAME = '' + + def validate_barcode(self, barcode_data): + """ + Default implementation returns False + """ + return False + + def __init__(self): + pass 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..f983487a41 --- /dev/null +++ b/InvenTree/plugins/barcode/inventree.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from . import barcode + + +class InvenTreeBarcodePlugin(barcode.BarcodePlugin): + + PLUGIN_NAME = "InvenTreeBarcodePlugin" + + def validate_barcode(self, barcode_data): + + print("testing") + + return True diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py new file mode 100644 index 0000000000..75815aded2 --- /dev/null +++ b/InvenTree/plugins/plugins.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +import inspect +import importlib +import pkgutil + +# Barcode plugins +import plugins.barcode as barcode +from plugins.barcode.barcode import BarcodePlugin + + +def iter_namespace(pkg): + + return pkgutil.iter_modules(pkg.__path__, pkg.__name__ + ".") + + +def get_modules(pkg): + # Return all modules in a given package + return [importlib.import_module(name) for finder, name, ispkg in iter_namespace(barcode)] + + +def get_classes(module): + # Return all classes in a given module + return inspect.getmembers(module, inspect.isclass) + + +def get_plugins(pkg, baseclass): + """ + Return a list of all modules under a given package. + + - Modules must be a subclass of the provided 'baseclass' + - Modules must have a non-empty PLUGIN_NAME parameter + """ + + plugins = [] + + modules = get_modules(pkg) + + # Iterate through each module in the package + for mod in modules: + # Iterate through each class in the module + for item in get_classes(mod): + plugin = item[1] + if plugin.__class__ is type(baseclass) and plugin.PLUGIN_NAME: + plugins.append(plugin) + + return plugins + + +def load_barcode_plugins(): + + return get_plugins(barcode, BarcodePlugin) From 38fab9c68155a468b1b185929682977c673695d2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 01:18:30 +1000 Subject: [PATCH 03/23] Test API info endpoint --- InvenTree/InvenTree/test_api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 0851815cfd..b7dbed8c76 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -36,8 +36,12 @@ class APITests(APITestCase): response = self.client.get(url, format='json') - print(response) - print(dir(response)) + 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 """ From cb1298847e4fafbf6cd1b4f7b7b4833ca867ae0a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 01:18:57 +1000 Subject: [PATCH 04/23] Load barcode plugins and throw test data at them --- InvenTree/InvenTree/api.py | 30 ++++++++++++++++++++++++++---- InvenTree/InvenTree/test_api.py | 4 ++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index e242f07e0f..6cdfbf0397 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -6,10 +6,14 @@ Main JSON interface views from __future__ import unicode_literals from django.http import JsonResponse +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 + class InfoView(AjaxView): """ Simple JSON endpoint for InvenTree information. @@ -27,15 +31,33 @@ class InfoView(AjaxView): return JsonResponse(data) -class BarcodeScanView(AjaxView): +class BarcodeScanView(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). + """ - def get(self, request, *args, **kwargs): - + def post(self, request, *args, **kwargs): + data = { 'barcode': 'Hello world', } - return JsonResponse(data) + plugins = inventree_plugins.load_barcode_plugins() + + for plugin in plugins: + print("Testing plugin:", plugin.PLUGIN_NAME) + if plugin().validate_barcode(request.data): + print("success!") + + return Response({ + 'success': 'OK', + 'data': data, + 'post': request.data, + }) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index b7dbed8c76..5b13663897 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -83,5 +83,5 @@ class APITests(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) def test_barcode(self): - - url = reverse('api-barcode-view') \ No newline at end of file + # TODO - Complete this + pass From 70589b06e1f1c6fa3618c1f6f1fa8daba2862982 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 19:27:43 +1000 Subject: [PATCH 05/23] doc --- InvenTree/plugins/plugins.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py index 75815aded2..a2f1c836aa 100644 --- a/InvenTree/plugins/plugins.py +++ b/InvenTree/plugins/plugins.py @@ -48,5 +48,8 @@ def get_plugins(pkg, baseclass): def load_barcode_plugins(): + """ + Return a list of all registered barcode plugins + """ return get_plugins(barcode, BarcodePlugin) From 4a615e05ae61aa92118821b00682ff0b07760cf0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 21:30:43 +1000 Subject: [PATCH 06/23] Further barcode work - Simplify InvenTree barcode format - Create base-clas for plugin --- InvenTree/InvenTree/api.py | 60 ++++++++++++++++++++------ InvenTree/InvenTree/helpers.py | 15 +++---- InvenTree/InvenTree/tests.py | 16 +++---- InvenTree/part/models.py | 8 ++-- InvenTree/plugins/barcode/barcode.py | 16 ++++--- InvenTree/plugins/barcode/inventree.py | 19 +++++++- InvenTree/plugins/plugin.py | 16 +++++++ InvenTree/plugins/plugins.py | 10 ++++- InvenTree/stock/models.py | 16 +++---- 9 files changed, 127 insertions(+), 49 deletions(-) create mode 100644 InvenTree/plugins/plugin.py diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 6cdfbf0397..af8290b236 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -5,7 +5,9 @@ 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.response import Response from rest_framework.views import APIView @@ -14,6 +16,11 @@ 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() + class InfoView(AjaxView): """ Simple JSON endpoint for InvenTree information. @@ -45,19 +52,46 @@ class BarcodeScanView(APIView): def post(self, request, *args, **kwargs): - data = { - 'barcode': 'Hello world', - } + response = None - plugins = inventree_plugins.load_barcode_plugins() + barcode_data = request.data - for plugin in plugins: - print("Testing plugin:", plugin.PLUGIN_NAME) - if plugin().validate_barcode(request.data): - print("success!") + print("Barcode data:") + print(barcode_data) - return Response({ - 'success': 'OK', - 'data': data, - 'post': request.data, - }) + if type(barcode_data) is not dict: + response = { + 'error': _('Barcode data could not be parsed'), + } + + else: + # Look for a barcode plugin that knows how to handle the data + for plugin_class in barcode_plugins: + + plugin = plugin_class() + + if plugin.validate_barcode(barcode_data): + + # Plugin should return a dict response + response = plugin.decode_barcode(barcode_data) + + if type(response) is dict: + response['success'] = _('Barcode successfully decoded') + else: + response = { + 'error': _('Barcode plugin returned incorrect response') + } + + response['plugin'] = plugin.get_name() + + break + + if response is None: + response = { + 'error': _('Unknown barcode format'), + } + + # Include the original barcode data + response['barcode_data'] = barcode_data + + 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/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/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/plugins/barcode/barcode.py b/InvenTree/plugins/barcode/barcode.py index a2d3c6652e..447a8d4edc 100644 --- a/InvenTree/plugins/barcode/barcode.py +++ b/InvenTree/plugins/barcode/barcode.py @@ -1,19 +1,25 @@ # -*- coding: utf-8 -*- +import plugins.plugin as plugin -class BarcodePlugin: + +class BarcodePlugin(plugin.InvenTreePlugin): """ The BarcodePlugin class is the base class for any barcode plugin. """ - # Override this for each actual plugin - PLUGIN_NAME = '' - def validate_barcode(self, barcode_data): """ Default implementation returns False """ return False + def decode_barcode(self, barcode_data): + """ + Decode the barcode, and craft a response + """ + + return None + def __init__(self): - pass + plugin.InvenTreePlugin.__init__(self) diff --git a/InvenTree/plugins/barcode/inventree.py b/InvenTree/plugins/barcode/inventree.py index f983487a41..b13e0643a3 100644 --- a/InvenTree/plugins/barcode/inventree.py +++ b/InvenTree/plugins/barcode/inventree.py @@ -8,7 +8,24 @@ class InvenTreeBarcodePlugin(barcode.BarcodePlugin): PLUGIN_NAME = "InvenTreeBarcodePlugin" def validate_barcode(self, barcode_data): + """ + An "InvenTree" barcode must include the following tags: - print("testing") + { + 'tool': 'InvenTree', + 'version': + } + + """ + + for key in ['tool', 'version']: + if key not in barcode_data.keys(): + return False + + if not barcode_data['tool'] == 'InvenTree': + return False return True + + def decode_barcode(self, barcode_data): + pass diff --git a/InvenTree/plugins/plugin.py b/InvenTree/plugins/plugin.py new file mode 100644 index 0000000000..ec40b6d4cf --- /dev/null +++ b/InvenTree/plugins/plugin.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + + +class InvenTreePlugin(): + """ + Base class for a Barcode plugin + """ + + # Override the plugin name for each concrete plugin instance + PLUGIN_NAME = '' + + def get_name(self): + return self.PLUGIN_NAME + + def __init__(self): + pass diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py index a2f1c836aa..03e127933e 100644 --- a/InvenTree/plugins/plugins.py +++ b/InvenTree/plugins/plugins.py @@ -52,4 +52,12 @@ def load_barcode_plugins(): Return a list of all registered barcode plugins """ - return get_plugins(barcode, BarcodePlugin) + plugins = get_plugins(barcode, BarcodePlugin) + + if len(plugins) > 0: + print("Discovered {n} barcode plugins:".format(n=len(plugins))) + + for bp in plugins: + print(" - {bp}".format(bp=bp.PLUGIN_NAME)) + + return plugins diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index ff0cf7b21a..f7d1c0b147 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -44,11 +44,11 @@ class StockLocation(InvenTreeTree): """ Return a JSON string for formatting a barcode for this StockLocation object """ return helpers.MakeBarcode( - 'StockLocation', - self.id, - reverse('api-location-detail', kwargs={'pk': self.id}), + 'stocklocation', { - 'name': self.name, + "id": self.id, + "name": self.name, + "url": reverse('api-location-detail', kwargs={'pk': self.id}), } ) @@ -288,12 +288,10 @@ class StockItem(MPTTModel): """ return helpers.MakeBarcode( - 'StockItem', - self.id, - reverse('api-stock-detail', kwargs={'pk': self.id}), + "stockitem", { - 'part_id': self.part.id, - 'part_name': self.part.full_name + "id": self.id, + "url": reverse('api-stock-detail', kwargs={'pk': self.id}), } ) From 5de85defa7762c69fce56b4da9d789f6f5bfb14e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 22:00:58 +1000 Subject: [PATCH 07/23] Validation of InvenTree style barcodes --- InvenTree/InvenTree/api.py | 3 +- InvenTree/plugins/barcode/barcode.py | 21 +++++++++++-- InvenTree/plugins/barcode/inventree.py | 42 +++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index af8290b236..69d7695974 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -76,7 +76,8 @@ class BarcodeScanView(APIView): response = plugin.decode_barcode(barcode_data) if type(response) is dict: - response['success'] = _('Barcode successfully decoded') + 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') diff --git a/InvenTree/plugins/barcode/barcode.py b/InvenTree/plugins/barcode/barcode.py index 447a8d4edc..90d9cf848d 100644 --- a/InvenTree/plugins/barcode/barcode.py +++ b/InvenTree/plugins/barcode/barcode.py @@ -8,6 +8,9 @@ class BarcodePlugin(plugin.InvenTreePlugin): The BarcodePlugin class is the base class for any barcode plugin. """ + def __init__(self): + plugin.InvenTreePlugin.__init__(self) + def validate_barcode(self, barcode_data): """ Default implementation returns False @@ -21,5 +24,19 @@ class BarcodePlugin(plugin.InvenTreePlugin): return None - def __init__(self): - plugin.InvenTreePlugin.__init__(self) + def render_part(self, part): + return { + 'id': part.id, + 'name': part.full_name, + } + + def render_stock_location(self, loc): + return { + "id": loc.id + } + + def render_stock_item(self, item): + + return { + "id": item.id, + } diff --git a/InvenTree/plugins/barcode/inventree.py b/InvenTree/plugins/barcode/inventree.py index b13e0643a3..ba2b7e737d 100644 --- a/InvenTree/plugins/barcode/inventree.py +++ b/InvenTree/plugins/barcode/inventree.py @@ -2,6 +2,11 @@ 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): @@ -28,4 +33,39 @@ class InvenTreeBarcodePlugin(barcode.BarcodePlugin): return True def decode_barcode(self, barcode_data): - pass + + response = {} + + if 'part' in barcode_data.keys(): + id = barcode_data['part'].get('id', None) + + try: + part = Part.objects.get(id=id) + response['part'] = self.render_part(part) + except (ValueError, Part.DoesNotExist): + response['error'] = _('Part does not exist') + + elif 'stocklocation' in barcode_data.keys(): + id = barcode_data['stocklocation'].get('id', None) + + try: + loc = StockLocation.objects.get(id=id) + response['stocklocation'] = self.render_stock_location(loc) + except (ValueError, StockLocation.DoesNotExist): + response['error'] = _('StockLocation does not exist') + + elif 'stockitem' in barcode_data.keys(): + + id = barcode_data['stockitem'].get('id', None) + + try: + item = StockItem.objects.get(id=id) + response['stockitem'] = self.render_stock_item(item) + except (ValueError, StockItem.DoesNotExist): + response['error'] = _('StockItem does not exist') + + else: + response['error'] = _('No matching data') + + return response + From 94e400d0e1c986bdb8ef4f5071030b8d61f93842 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 22:30:29 +1000 Subject: [PATCH 08/23] Simplify barcode plugin class --- InvenTree/InvenTree/api.py | 7 ++++--- InvenTree/plugins/barcode/barcode.py | 17 ++++++++++++++--- InvenTree/plugins/barcode/inventree.py | 20 ++++++++++---------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 69d7695974..427bd4efdf 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -68,12 +68,13 @@ class BarcodeScanView(APIView): # Look for a barcode plugin that knows how to handle the data for plugin_class in barcode_plugins: - plugin = plugin_class() + # Instantiate the plugin with the provided plugin data + plugin = plugin_class(barcode_data) - if plugin.validate_barcode(barcode_data): + if plugin.validate(): # Plugin should return a dict response - response = plugin.decode_barcode(barcode_data) + response = plugin.decode() if type(response) is dict: if 'success' not in response.keys() and 'error' not in response.keys(): diff --git a/InvenTree/plugins/barcode/barcode.py b/InvenTree/plugins/barcode/barcode.py index 90d9cf848d..9482154857 100644 --- a/InvenTree/plugins/barcode/barcode.py +++ b/InvenTree/plugins/barcode/barcode.py @@ -8,16 +8,27 @@ class BarcodePlugin(plugin.InvenTreePlugin): The BarcodePlugin class is the base class for any barcode plugin. """ - def __init__(self): + def __init__(self, barcode_data): plugin.InvenTreePlugin.__init__(self) - def validate_barcode(self, barcode_data): + 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. + """ + + return "" + + def validate(self): """ Default implementation returns False """ return False - def decode_barcode(self, barcode_data): + def decode(self): """ Decode the barcode, and craft a response """ diff --git a/InvenTree/plugins/barcode/inventree.py b/InvenTree/plugins/barcode/inventree.py index ba2b7e737d..231edfe189 100644 --- a/InvenTree/plugins/barcode/inventree.py +++ b/InvenTree/plugins/barcode/inventree.py @@ -12,7 +12,7 @@ class InvenTreeBarcodePlugin(barcode.BarcodePlugin): PLUGIN_NAME = "InvenTreeBarcodePlugin" - def validate_barcode(self, barcode_data): + def validate(self): """ An "InvenTree" barcode must include the following tags: @@ -24,20 +24,20 @@ class InvenTreeBarcodePlugin(barcode.BarcodePlugin): """ for key in ['tool', 'version']: - if key not in barcode_data.keys(): + if key not in self.data.keys(): return False - if not barcode_data['tool'] == 'InvenTree': + if not self.data['tool'] == 'InvenTree': return False return True - def decode_barcode(self, barcode_data): + def decode(self): response = {} - if 'part' in barcode_data.keys(): - id = barcode_data['part'].get('id', None) + if 'part' in self.data.keys(): + id = self.data['part'].get('id', None) try: part = Part.objects.get(id=id) @@ -45,8 +45,8 @@ class InvenTreeBarcodePlugin(barcode.BarcodePlugin): except (ValueError, Part.DoesNotExist): response['error'] = _('Part does not exist') - elif 'stocklocation' in barcode_data.keys(): - id = barcode_data['stocklocation'].get('id', None) + elif 'stocklocation' in self.data.keys(): + id = self.data['stocklocation'].get('id', None) try: loc = StockLocation.objects.get(id=id) @@ -54,9 +54,9 @@ class InvenTreeBarcodePlugin(barcode.BarcodePlugin): except (ValueError, StockLocation.DoesNotExist): response['error'] = _('StockLocation does not exist') - elif 'stockitem' in barcode_data.keys(): + elif 'stockitem' in self.data.keys(): - id = barcode_data['stockitem'].get('id', None) + id = self.data['stockitem'].get('id', None) try: item = StockItem.objects.get(id=id) From f742f32804badf557043ea0e453b33c0bc951dce Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 22:33:03 +1000 Subject: [PATCH 09/23] Added some doc string --- InvenTree/plugins/barcode/inventree.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugins/barcode/inventree.py b/InvenTree/plugins/barcode/inventree.py index 231edfe189..a9125ed8a8 100644 --- a/InvenTree/plugins/barcode/inventree.py +++ b/InvenTree/plugins/barcode/inventree.py @@ -1,3 +1,14 @@ +""" +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 -*- from . import barcode @@ -68,4 +79,3 @@ class InvenTreeBarcodePlugin(barcode.BarcodePlugin): response['error'] = _('No matching data') return response - From 277b28a7e948a2b98ee1c9798d0ef0240f746093 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 22:38:32 +1000 Subject: [PATCH 10/23] Create a "unique(ish)" hash for barcode data --- InvenTree/InvenTree/api.py | 1 + InvenTree/plugins/barcode/barcode.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 427bd4efdf..2c5d3f1b0c 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -85,6 +85,7 @@ class BarcodeScanView(APIView): } response['plugin'] = plugin.get_name() + response['hash'] = plugin.hash() break diff --git a/InvenTree/plugins/barcode/barcode.py b/InvenTree/plugins/barcode/barcode.py index 9482154857..6710c0f78e 100644 --- a/InvenTree/plugins/barcode/barcode.py +++ b/InvenTree/plugins/barcode/barcode.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import hashlib + import plugins.plugin as plugin @@ -18,9 +20,17 @@ class BarcodePlugin(plugin.InvenTreePlugin): 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. + """ - return "" + hash = hashlib.md5(str(self.data).encode()) + return str(hash.hexdigest()) def validate(self): """ From ba4a1fd7711b523ab38953d40a1920ac81093f38 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 22:54:05 +1000 Subject: [PATCH 11/23] Add a 'uid' field to StockItem model - To be used for barcode asociation and lookup --- .../stock/migrations/0026_stockitem_uid.py | 18 ++++++++++++++++++ InvenTree/stock/models.py | 3 +++ 2 files changed, 21 insertions(+) create mode 100644 InvenTree/stock/migrations/0026_stockitem_uid.py diff --git a/InvenTree/stock/migrations/0026_stockitem_uid.py b/InvenTree/stock/migrations/0026_stockitem_uid.py new file mode 100644 index 0000000000..c00e858815 --- /dev/null +++ b/InvenTree/stock/migrations/0026_stockitem_uid.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-04-14 12:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0025_auto_20200405_2243'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='uid', + field=models.CharField(blank=True, help_text='Unique identifier field', max_length=128), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index f7d1c0b147..51b61ff3fd 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -108,6 +108,7 @@ class StockItem(MPTTModel): Attributes: parent: Link to another StockItem from which this StockItem was created + uid: Field containing a unique-id which is mapped to a third-party identifier (e.g. a barcode) part: Link to the master abstract part that this StockItem is an instance of supplier_part: Link to a specific SupplierPart (optional) location: Where this StockItem is located @@ -295,6 +296,8 @@ class StockItem(MPTTModel): } ) + uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field")) + parent = TreeForeignKey('self', on_delete=models.DO_NOTHING, blank=True, null=True, From 977316cb3a2a128b5da4bdb7b43040eff5bfb7a4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 22:57:46 +1000 Subject: [PATCH 12/23] Include UID field in StockItem API --- InvenTree/stock/api.py | 3 ++- InvenTree/stock/serializers.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index b9132ab557..9f485ba5f7 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -344,6 +344,7 @@ class StockList(generics.ListCreateAPIView): data = queryset.values( 'pk', + 'uid', 'parent', 'quantity', 'serial', @@ -540,7 +541,7 @@ class StockList(generics.ListCreateAPIView): 'supplier_part', 'customer', 'belongs_to', - 'build' + 'build', ] diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index ce4041fec3..fe4f850658 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -39,6 +39,7 @@ class StockItemSerializerBrief(InvenTreeModelSerializer): model = StockItem fields = [ 'pk', + 'uid', 'part', 'part_name', 'supplier_part', @@ -106,6 +107,7 @@ class StockItemSerializer(InvenTreeModelSerializer): 'status', 'status_text', 'tracking_items', + 'uid', 'url', ] From bad56f64e39607bcdde2022dd4cccf7db3fb72da Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 23:22:57 +1000 Subject: [PATCH 13/23] Server does more of the heavy-lifting of the barcode decoding --- InvenTree/InvenTree/api.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 2c5d3f1b0c..b682e775cd 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -5,6 +5,8 @@ Main JSON interface views # -*- coding: utf-8 -*- from __future__ import unicode_literals +import json + from django.utils.translation import ugettext as _ from django.http import JsonResponse @@ -52,19 +54,33 @@ class BarcodeScanView(APIView): def post(self, request, *args, **kwargs): - response = None + response = {} - barcode_data = request.data + barcode_data = request.data.get('barcode', None) print("Barcode data:") print(barcode_data) - if type(barcode_data) is not dict: - response = { - 'error': _('Barcode data could not be parsed'), - } + valid_data = False + + if barcode_data is None: + response['error'] = _('No barcode data provided') + + elif type(barcode_data) is dict: + valid_data = True + + elif type(barcode_data) is str: + # Attempt to decode the barcode into a JSON object + try: + barcode_data = json.loads(barcode_data) + valid_data = True + except json.JSONDecodeError: + response['error'] = _('Barcode is not a JSON object') else: + response['error'] = _('Barcode data is unknown format') + + if valid_data: # Look for a barcode plugin that knows how to handle the data for plugin_class in barcode_plugins: @@ -89,7 +105,7 @@ class BarcodeScanView(APIView): break - if response is None: + if 'error' not in response and 'success' not in response: response = { 'error': _('Unknown barcode format'), } From e56c018a4ac4e70f1f8f6b068966a66d32047b8c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 23:28:46 +1000 Subject: [PATCH 14/23] Display StockItem UID if one exists --- InvenTree/stock/templates/stock/item_base.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 46e26b6ff1..ee73f8565e 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -85,7 +85,7 @@ {% if item.belongs_to %} - + {% trans "Belongs To" %} {{ item.belongs_to }} @@ -96,6 +96,13 @@ {{ item.location.name }} {% endif %} + {% if item.uid %} + + + {% trans "Unique Identifier" %} + {{ item.uid }} + + {% endif %} {% if item.serialized %} From 7faa0d199df405e3f8c92a6618281cb3e1d1a2ed Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 23:33:03 +1000 Subject: [PATCH 15/23] Push even more barcode decoding to the individual plugin - DigiKey barcode is NOT json formatted, for example... --- InvenTree/InvenTree/api.py | 57 +++++++++----------------- InvenTree/plugins/barcode/inventree.py | 13 ++++++ 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index b682e775cd..b137100327 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -5,8 +5,6 @@ Main JSON interface views # -*- coding: utf-8 -*- from __future__ import unicode_literals -import json - from django.utils.translation import ugettext as _ from django.http import JsonResponse @@ -61,49 +59,32 @@ class BarcodeScanView(APIView): print("Barcode data:") print(barcode_data) - valid_data = False - if barcode_data is None: response['error'] = _('No barcode data provided') - elif type(barcode_data) is dict: - valid_data = True + # Look for a barcode plugin that knows how to handle the data + for plugin_class in barcode_plugins: - elif type(barcode_data) is str: - # Attempt to decode the barcode into a JSON object - try: - barcode_data = json.loads(barcode_data) - valid_data = True - except json.JSONDecodeError: - response['error'] = _('Barcode is not a JSON object') + # Instantiate the plugin with the provided plugin data + plugin = plugin_class(barcode_data) - else: - response['error'] = _('Barcode data is unknown format') + 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') + } - if valid_data: - # Look for a barcode plugin that knows how to handle the data - for plugin_class in barcode_plugins: + response['plugin'] = plugin.get_name() + response['hash'] = plugin.hash() - # 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.get_name() - response['hash'] = plugin.hash() - - break + break if 'error' not in response and 'success' not in response: response = { diff --git a/InvenTree/plugins/barcode/inventree.py b/InvenTree/plugins/barcode/inventree.py index a9125ed8a8..93b86d42b7 100644 --- a/InvenTree/plugins/barcode/inventree.py +++ b/InvenTree/plugins/barcode/inventree.py @@ -11,6 +11,8 @@ references model objects actually exist in the database. # -*- coding: utf-8 -*- +import json + from . import barcode from stock.models import StockItem, StockLocation @@ -34,6 +36,17 @@ class InvenTreeBarcodePlugin(barcode.BarcodePlugin): """ + # The data must either be dict or be able to dictified + if type(self.data) is dict: + pass + elif type(self.data) is str: + try: + self.data = json.loads(self.data) + except json.JSONDecodeError: + return False + else: + return False + for key in ['tool', 'version']: if key not in self.data.keys(): return False From 4d7407ee512cacfce4df85cf2f8d6c4dd7e0b15d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 23:38:42 +1000 Subject: [PATCH 16/23] Logic fix --- InvenTree/InvenTree/api.py | 40 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index b137100327..af66fa6751 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -61,30 +61,30 @@ class BarcodeScanView(APIView): 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: - # 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) - # 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') + } - 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.get_name() + response['hash'] = plugin.hash() - response['plugin'] = plugin.get_name() - response['hash'] = plugin.hash() - - break + break if 'error' not in response and 'success' not in response: response = { From a58e2e84f8ab07448f73a844e8675107a9a11c66 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 00:16:42 +1000 Subject: [PATCH 17/23] Add "ActionPlugin" interface - Plugin for running a custom action --- InvenTree/InvenTree/api.py | 44 +++++++++++++- InvenTree/InvenTree/urls.py | 7 ++- InvenTree/plugins/action/__init__.py | 0 InvenTree/plugins/action/action.py | 87 ++++++++++++++++++++++++++++ InvenTree/plugins/plugin.py | 2 +- InvenTree/plugins/plugins.py | 28 ++++++++- 6 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 InvenTree/plugins/action/__init__.py create mode 100644 InvenTree/plugins/action/action.py diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index af66fa6751..4103fd290a 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -8,6 +8,7 @@ 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 @@ -20,6 +21,7 @@ from plugins import plugins as inventree_plugins print("INFO: Loading plugins") barcode_plugins = inventree_plugins.load_barcode_plugins() +action_plugins = inventree_plugins.load_action_plugins() class InfoView(AjaxView): @@ -38,7 +40,43 @@ class InfoView(AjaxView): return JsonResponse(data) -class BarcodeScanView(APIView): +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 for"), + "action": action, + }) + + +class BarcodePluginView(APIView): """ Endpoint for handling barcode scan requests. @@ -50,6 +88,10 @@ class BarcodeScanView(APIView): """ + permission_classes = [ + permissions.IsAuthenticated, + ] + def post(self, request, *args, **kwargs): response = {} diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 1d1fabc795..d9600333f4 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -36,7 +36,7 @@ from rest_framework.documentation import include_docs_urls from .views import IndexView, SearchView, DatabaseStatsView from .views import SettingsView, EditUserView, SetPasswordView -from .api import InfoView, BarcodeScanView +from .api import InfoView, BarcodePluginView, ActionPluginView from users.urls import user_urls @@ -54,8 +54,9 @@ apipatterns = [ # User URLs url(r'^user/', include(user_urls)), - # Barcode scanning endpoint - url(r'^barcode/', BarcodeScanView.as_view(), name='api-barcode-scan'), + # 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='api-inventree-info'), 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/plugin.py b/InvenTree/plugins/plugin.py index ec40b6d4cf..11de4d1365 100644 --- a/InvenTree/plugins/plugin.py +++ b/InvenTree/plugins/plugin.py @@ -9,7 +9,7 @@ class InvenTreePlugin(): # Override the plugin name for each concrete plugin instance PLUGIN_NAME = '' - def get_name(self): + def plugin_name(self): return self.PLUGIN_NAME def __init__(self): diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py index 03e127933e..f913c1f295 100644 --- a/InvenTree/plugins/plugins.py +++ b/InvenTree/plugins/plugins.py @@ -8,6 +8,10 @@ import pkgutil import plugins.barcode as barcode from plugins.barcode.barcode import BarcodePlugin +# Action plugins +import plugins.action as action +from plugins.action.action import ActionPlugin + def iter_namespace(pkg): @@ -16,7 +20,7 @@ def iter_namespace(pkg): def get_modules(pkg): # Return all modules in a given package - return [importlib.import_module(name) for finder, name, ispkg in iter_namespace(barcode)] + return [importlib.import_module(name) for finder, name, ispkg in iter_namespace(pkg)] def get_classes(module): @@ -41,7 +45,7 @@ def get_plugins(pkg, baseclass): # Iterate through each class in the module for item in get_classes(mod): plugin = item[1] - if plugin.__class__ is type(baseclass) and plugin.PLUGIN_NAME: + if issubclass(plugin, baseclass) and plugin.PLUGIN_NAME: plugins.append(plugin) return plugins @@ -52,6 +56,8 @@ def load_barcode_plugins(): Return a list of all registered barcode plugins """ + print("Loading barcode plugins") + plugins = get_plugins(barcode, BarcodePlugin) if len(plugins) > 0: @@ -61,3 +67,21 @@ def load_barcode_plugins(): print(" - {bp}".format(bp=bp.PLUGIN_NAME)) return plugins + + +def load_action_plugins(): + """ + Return a list of all registered action plugins + """ + + print("Loading action plugins") + + plugins = get_plugins(action, ActionPlugin) + + if len(plugins) > 0: + print("Discovered {n} action plugins:".format(n=len(plugins))) + + for ap in plugins: + print(" - {ap}".format(ap=ap.PLUGIN_NAME)) + + return plugins From d57fed614289bca9dea5234d289cc60ece99666a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 00:30:00 +1000 Subject: [PATCH 18/23] Change fingerprint icon to barcode --- InvenTree/stock/templates/stock/item_base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index ee73f8565e..9785b78850 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -98,7 +98,7 @@ {% endif %} {% if item.uid %} - + {% trans "Unique Identifier" %} {{ item.uid }} From 44addc9d7f26bfe0c82548f6fbfafd0d165384db Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 07:54:38 +1000 Subject: [PATCH 19/23] Bugfix --- InvenTree/InvenTree/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 4103fd290a..02975c3088 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -123,7 +123,7 @@ class BarcodePluginView(APIView): 'error': _('Barcode plugin returned incorrect response') } - response['plugin'] = plugin.get_name() + response['plugin'] = plugin.plugin_name() response['hash'] = plugin.hash() break From c12a482e4de9c84950630cdfcc1c50d52e9bc226 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 13:32:14 +1000 Subject: [PATCH 20/23] Add "supplier_reference" field to PurchaseOrder - This is the code that the Supplier uses for the particuarl sales order --- .../0019_purchaseorder_supplier_reference.py | 18 ++++++++++++++++++ InvenTree/order/models.py | 4 +++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 InvenTree/order/migrations/0019_purchaseorder_supplier_reference.py diff --git a/InvenTree/order/migrations/0019_purchaseorder_supplier_reference.py b/InvenTree/order/migrations/0019_purchaseorder_supplier_reference.py new file mode 100644 index 0000000000..cf9cd345bc --- /dev/null +++ b/InvenTree/order/migrations/0019_purchaseorder_supplier_reference.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.10 on 2020-04-15 03:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0018_auto_20200406_0151'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorder', + name='supplier_reference', + field=models.CharField(blank=True, help_text='Supplier order reference', max_length=64), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 9b569aa4cb..3a7d65abac 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -119,7 +119,7 @@ class PurchaseOrder(Order): supplier: Reference to the company supplying the goods in the order received_by: User that received the goods """ - + ORDER_PREFIX = "PO" supplier = models.ForeignKey( @@ -131,6 +131,8 @@ class PurchaseOrder(Order): help_text=_('Company') ) + supplier_reference = models.CharField(max_length=64, blank=True, help_text=_("Supplier order reference")) + received_by = models.ForeignKey( User, on_delete=models.SET_NULL, From 610f85597f09f8cc6e97e20d7f1c6382a5518711 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 14:18:17 +1000 Subject: [PATCH 21/23] Expose supplier_reference to external API --- InvenTree/order/api.py | 1 + InvenTree/order/serializers.py | 1 + 2 files changed, 2 insertions(+) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 21fbd80326..18ba890127 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -74,6 +74,7 @@ class POList(generics.ListCreateAPIView): data = queryset.values( 'pk', 'supplier', + 'supplier_reference', 'supplier__name', 'supplier__image', 'reference', diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index ae6ace2148..9a8f1afee5 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -19,6 +19,7 @@ class POSerializer(InvenTreeModelSerializer): fields = [ 'pk', 'supplier', + 'supplier_reference', 'reference', 'description', 'link', From d19e287cb52282b791cee4aba232895397a9e55f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 14:24:59 +1000 Subject: [PATCH 22/23] Template rendering improvements --- .../templates/company/supplier_part_base.html | 6 +++--- InvenTree/order/forms.py | 1 + .../order/templates/order/order_base.html | 20 +++++++++++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) 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" %} {{ part.supplier.name }} - + {% trans "SKU" %} {{ part.SKU }} @@ -71,14 +71,14 @@ InvenTree | {% trans "Supplier Part" %} {% trans "Manufacturer" %} {{ part.manufacturer.name }} - + {% trans "MPN" %} {{ part.MPN }} {% endif %} {% if part.note %} - + {% trans "Note" %} {{ part.note }} diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index c110dfadca..52c761e03e 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -69,6 +69,7 @@ class EditPurchaseOrderForm(HelperForm): fields = [ 'reference', 'supplier', + 'supplier_reference', 'description', 'link', ] diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 420f312310..03aa4c4ce2 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -63,15 +63,27 @@ InvenTree | {{ order }} - - - + + + - + + + + + + + {% if order.supplier_reference %} + + + + + + {% endif %} {% if order.link %} From 10ee8bc666c91aa6327e12f53f25cd2d28669bab Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 23:41:16 +1000 Subject: [PATCH 23/23] Use existing serializers to encode information for barcode response --- InvenTree/InvenTree/api.py | 5 +++- InvenTree/part/serializers.py | 2 ++ InvenTree/plugins/barcode/barcode.py | 38 ++++++++++++++++++++-------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 02975c3088..d68ecd67ad 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -71,7 +71,7 @@ class ActionPluginView(APIView): # If we got to here, no matching action was found return Response({ - 'error': _("No matching action found for"), + 'error': _("No matching action found"), "action": action, }) @@ -136,4 +136,7 @@ class BarcodePluginView(APIView): # Include the original barcode data response['barcode_data'] = barcode_data + print("Response:") + print(response) + return Response(response) 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/barcode/barcode.py b/InvenTree/plugins/barcode/barcode.py index 6710c0f78e..f8bd82f744 100644 --- a/InvenTree/plugins/barcode/barcode.py +++ b/InvenTree/plugins/barcode/barcode.py @@ -2,6 +2,11 @@ import hashlib +from rest_framework.renderers import JSONRenderer + +from stock.serializers import StockItemSerializer, LocationSerializer +from part.serializers import PartSerializer + import plugins.plugin as plugin @@ -46,18 +51,31 @@ class BarcodePlugin(plugin.InvenTreePlugin): return None def render_part(self, part): - return { - 'id': part.id, - 'name': part.full_name, - } + """ + 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): - return { - "id": loc.id - } + """ + 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 + """ - return { - "id": item.id, - } + serializer = StockItemSerializer(item, part_detail=True, location_detail=True, supplier_detail=True) + + return serializer.data
{% 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 }}