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" %} {{ 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/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/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/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, 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', 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 %} 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': + } + + """ + + # 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 + + if not self.data['tool'] == 'InvenTree': + return False + + return True + + def decode(self): + + response = {} + + if 'part' in self.data.keys(): + id = self.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 self.data.keys(): + id = self.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 self.data.keys(): + + id = self.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 diff --git a/InvenTree/plugins/plugin.py b/InvenTree/plugins/plugin.py new file mode 100644 index 0000000000..11de4d1365 --- /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 plugin_name(self): + return self.PLUGIN_NAME + + def __init__(self): + pass diff --git a/InvenTree/plugins/plugins.py b/InvenTree/plugins/plugins.py new file mode 100644 index 0000000000..f913c1f295 --- /dev/null +++ b/InvenTree/plugins/plugins.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +import inspect +import importlib +import pkgutil + +# Barcode plugins +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): + + 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(pkg)] + + +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 issubclass(plugin, baseclass) and plugin.PLUGIN_NAME: + plugins.append(plugin) + + return plugins + + +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: + print("Discovered {n} barcode plugins:".format(n=len(plugins))) + + for bp in 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 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/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 ff0cf7b21a..51b61ff3fd 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}), } ) @@ -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 @@ -288,15 +289,15 @@ 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}), } ) + 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, 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', ] diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 46e26b6ff1..9785b78850 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 %} - + @@ -96,6 +96,13 @@ {% endif %} + {% if item.uid %} + + + + + + {% endif %} {% if item.serialized %}
{% 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 }}
{% trans "Belongs To" %} {{ item.belongs_to }}
{{ item.location.name }}
{% trans "Unique Identifier" %}{{ item.uid }}