From b850beb687def415973239f40a568a207ee72392 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 13 Apr 2020 21:24:36 +1000 Subject: [PATCH 01/37] Add ability to filter by 'starred' status --- .../InvenTree/static/script/inventree/part.js | 2 +- InvenTree/part/api.py | 54 +++++++++++++------ InvenTree/part/templates/part/category.html | 16 +++--- 3 files changed, 46 insertions(+), 26 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/part.js b/InvenTree/InvenTree/static/script/inventree/part.js index 05b209b9b8..927afca2e6 100644 --- a/InvenTree/InvenTree/static/script/inventree/part.js +++ b/InvenTree/InvenTree/static/script/inventree/part.js @@ -87,7 +87,7 @@ function loadPartTable(table, url, options={}) { * buttons: If provided, link buttons to selection status of this table */ - var params = options.parms || {}; + var params = options.params || {}; var filters = loadTableFilters("parts"); diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 7d193fa1ee..629136af05 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -153,6 +153,7 @@ class PartList(generics.ListCreateAPIView): The Part object list can be filtered by: - category: Filter by PartCategory reference - cascade: If true, include parts from sub-categories + - starred: Is the part "starred" by the current user? - is_template: Is the part a template part? - variant_of: Filter by variant_of Part reference - assembly: Filter by assembly field @@ -295,31 +296,50 @@ class PartList(generics.ListCreateAPIView): def get_queryset(self): - # Does the user wish to filter by category? - cat_id = self.request.query_params.get('category', None) # Start with all objects parts_list = Part.objects.all() - cascade = str2bool(self.request.query_params.get('cascade', False)) + # Filter by 'starred' parts? + starred = str2bool(self.request.query_params.get('starred', None)) + + if starred is not None: + starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()] + + if starred: + parts_list = parts_list.filter(pk__in=starred_parts) + else: + parts_list = parts_list.exclude(pk__in=starred_parts) + + cascade = str2bool(self.request.query_params.get('cascade', None)) + + # Does the user wish to filter by category? + cat_id = self.request.query_params.get('category', None) if cat_id is None: - # Top-level parts - if not cascade: - parts_list = parts_list.filter(category=None) - + # No category filtering if category is not specified + pass + else: - try: - category = PartCategory.objects.get(pk=cat_id) + # Category has been specified! + if isNull(cat_id): + # A 'null' category is the top-level category + if cascade is False: + # Do not cascade, only list parts in the top-level category + parts_list = parts_list.filter(category=None) - # If '?cascade=true' then include parts which exist in sub-categories - if cascade: - parts_list = parts_list.filter(category__in=category.getUniqueChildren()) - # Just return parts directly in the requested category - else: - parts_list = parts_list.filter(category=cat_id) - except (ValueError, PartCategory.DoesNotExist): - pass + else: + try: + category = PartCategory.objects.get(pk=cat_id) + + # If '?cascade=true' then include parts which exist in sub-categories + if cascade: + parts_list = parts_list.filter(category__in=category.getUniqueChildren()) + # Just return parts directly in the requested category + else: + parts_list = parts_list.filter(category=cat_id) + except (ValueError, PartCategory.DoesNotExist): + pass # Ensure that related models are pre-loaded to reduce DB trips parts_list = self.get_serializer_class().setup_eager_loading(parts_list) diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index cc663a63c4..63a60bd71e 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -200,11 +200,11 @@ {% if category %} $("#cat-edit").click(function () { launchModalForm( - "{% url 'category-edit' category.id %}", - { - reload: true - }, - ); + "{% url 'category-edit' category.id %}", + { + reload: true + }, + ); return false; }); @@ -227,9 +227,9 @@ "#part-table", "{% url 'api-part-list' %}", { - query: { - {% if category %} - category: {{ category.id }}, + params: { + {% if category %}category: {{ category.id }}, + {% else %}category: "null", {% endif %} }, buttons: ['#part-options'], From 124fab3eeedea4b50ffea0d527b249868390e470 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 13 Apr 2020 21:30:17 +1000 Subject: [PATCH 02/37] Display a part as 'starred' in the part table --- InvenTree/InvenTree/static/script/inventree/part.js | 4 ++++ InvenTree/part/api.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/InvenTree/InvenTree/static/script/inventree/part.js b/InvenTree/InvenTree/static/script/inventree/part.js index 927afca2e6..05328dbbd8 100644 --- a/InvenTree/InvenTree/static/script/inventree/part.js +++ b/InvenTree/InvenTree/static/script/inventree/part.js @@ -147,6 +147,10 @@ function loadPartTable(table, url, options={}) { display += ``; } + if (row.starred) { + display += ``; + } + /* if (row.component) { display = display + ``; diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 629136af05..6615b4df43 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -258,12 +258,18 @@ class PartList(generics.ListCreateAPIView): # Filter items which have an 'in_stock' level higher than 'minimum_stock' data = data.filter(Q(in_stock__gte=F('minimum_stock'))) + # Get a list of the parts that this user has starred + starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()] + # Reduce the number of lookups we need to do for the part categories categories = {} for item in data: if item['image']: + # Is this part 'starred' for the current user? + item['starred'] = item['pk'] in starred_parts + img = item['image'] # Use the 'thumbnail' image here instead of the full-size image From 90ac3a5a8ac3ce574dd35f9a4fbba27399f1c0b3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 13 Apr 2020 21:30:34 +1000 Subject: [PATCH 03/37] Add custom user filter for 'starred' status --- InvenTree/templates/table_filters.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/templates/table_filters.html b/InvenTree/templates/table_filters.html index e3dd78be19..f976e977a6 100644 --- a/InvenTree/templates/table_filters.html +++ b/InvenTree/templates/table_filters.html @@ -89,6 +89,10 @@ function getAvailableTableFilters(tableKey) { type: 'bool', title: '{% trans "Component" %}', }, + starred: { + type: 'bool', + title: '{% trans "Starred" %}', + }, }; } From 0e55911a6ba38f47fdea6123eb87d6e6f16f5f8f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 13 Apr 2020 22:07:14 +1000 Subject: [PATCH 04/37] Index page rendering is now a lot faster - Hide some elements which are currently very expensive to compute - --- InvenTree/InvenTree/views.py | 7 ++-- InvenTree/part/api.py | 4 +- InvenTree/templates/InvenTree/index.html | 38 ++++++++++++------- InvenTree/templates/InvenTree/low_stock.html | 15 ++++++++ .../templates/InvenTree/parts_to_order.html | 15 -------- .../templates/InvenTree/starred_parts.html | 12 +++--- InvenTree/templates/base.html | 1 + 7 files changed, 53 insertions(+), 39 deletions(-) create mode 100644 InvenTree/templates/InvenTree/low_stock.html delete mode 100644 InvenTree/templates/InvenTree/parts_to_order.html diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index e1258385a5..59833d3e6b 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -494,15 +494,16 @@ class IndexView(TemplateView): context = super(TemplateView, self).get_context_data(**kwargs) - context['starred'] = [star.part for star in self.request.user.starred_parts.all()] + # TODO - Re-implement this when a less expensive method is worked out + # context['starred'] = [star.part for star in self.request.user.starred_parts.all()] # Generate a list of orderable parts which have stock below their minimum values # TODO - Is there a less expensive way to get these from the database - context['to_order'] = [part for part in Part.objects.filter(purchaseable=True) if part.need_to_restock()] + # context['to_order'] = [part for part in Part.objects.filter(purchaseable=True) if part.need_to_restock()] # Generate a list of assembly parts which have stock below their minimum values # TODO - Is there a less expensive way to get these from the database - context['to_build'] = [part for part in Part.objects.filter(assembly=True) if part.need_to_restock()] + # context['to_build'] = [part for part in Part.objects.filter(assembly=True) if part.need_to_restock()] return context diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 6615b4df43..e36d4a568b 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -301,7 +301,9 @@ class PartList(generics.ListCreateAPIView): return Response(data) def get_queryset(self): - + """ + Implement custom filtering for the Part list API + """ # Start with all objects parts_list = Part.objects.all() diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index c4eb5990cc..570378e55d 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -9,13 +9,7 @@ InvenTree | Index
{% include "InvenTree/starred_parts.html" with collapse_id="starred" %} -{% if to_order %} -{% include "InvenTree/parts_to_order.html" with collapse_id="order" %} -{% endif %} - -{% if to_build %} -{% include "InvenTree/parts_to_build.html" with collapse_id="build" %} -{% endif %} +{% include "InvenTree/low_stock.html" with collapse_id="order" %} {% endblock %} @@ -25,15 +19,31 @@ InvenTree | Index {% block js_ready %} -console.log("abcde?"); - {{ block.super }} -//TODO: These calls to bootstrapTable() are failing, for some reason? -//$("#to-build-table").bootstrapTable(); -//$("#to-order-table").bootstrapTable(); -//$("#starred-parts-table").bootstrapTable(); +loadPartTable("#starred-parts-table", "{% url 'api-part-list' %}", { + params: { + "starred": true, + } +}); + +loadPartTable("#low-stock-table", "{% url 'api-part-list' %}", { + params: { + "low_stock": true, + } +}); + +$("#starred-parts-table").on('load-success.bs.table', function() { + var count = $("#starred-parts-table").bootstrapTable('getData').length; + + $("#starred-parts-count").html(count); +}); + +$("#low-stock-table").on('load-success.bs.table', function() { + var count = $("#low-stock-table").bootstrapTable('getData').length; + + $("#low-stock-count").html(count); +}); -console.log("Got to here..."); {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/low_stock.html b/InvenTree/templates/InvenTree/low_stock.html new file mode 100644 index 0000000000..edafab1756 --- /dev/null +++ b/InvenTree/templates/InvenTree/low_stock.html @@ -0,0 +1,15 @@ +{% extends "collapse.html" %} + +{% load i18n %} + +{% block collapse_title %} + +{% trans "Low Stock" %}0 +{% endblock %} + +{% block collapse_content %} + + +
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/parts_to_order.html b/InvenTree/templates/InvenTree/parts_to_order.html deleted file mode 100644 index 5d2c3472b4..0000000000 --- a/InvenTree/templates/InvenTree/parts_to_order.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "collapse.html" %} -{% block collapse_title %} - -Parts to Order{{ to_order | length }} -{% endblock %} - -{% block collapse_heading %} -There are {{ to_order | length }} parts which need to be ordered. -{% endblock %} - -{% block collapse_content %} - -{% include "required_part_table.html" with parts=to_order table_id="to-order-table" %} - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/starred_parts.html b/InvenTree/templates/InvenTree/starred_parts.html index 091afde064..f13987e3c5 100644 --- a/InvenTree/templates/InvenTree/starred_parts.html +++ b/InvenTree/templates/InvenTree/starred_parts.html @@ -1,15 +1,15 @@ {% extends "collapse.html" %} + +{% load i18n %} + {% block collapse_title %} -Starred Parts{{ starred | length }} -{% endblock %} - -{% block collapse_heading %} -You have {{ starred | length }} favourite parts +{% trans "Starred Parts" %}0 {% endblock %} {% block collapse_content %} -{% include "required_part_table.html" with parts=starred table_id="starred-parts-table" %} + +
{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 3cae9fd37b..8559e6d5f1 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -102,6 +102,7 @@ InvenTree + From 47530b7d2a25afd1ebad28a0cc51aa37bad00846 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 13 Apr 2020 22:21:20 +1000 Subject: [PATCH 05/37] Improvements for "SupplierPartCreate" form --- .../templates/company/detail_part.html | 19 ++++++++++++++++--- InvenTree/company/views.py | 19 +++++++++++-------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html index 537f7b07c3..2364f36b61 100644 --- a/InvenTree/company/templates/company/detail_part.html +++ b/InvenTree/company/templates/company/detail_part.html @@ -33,16 +33,29 @@ "{% url 'supplier-part-create' %}", { data: { - supplier: {{ company.id }} + {% if company.is_supplier %}supplier: {{ company.id }},{% endif %} + {% if company.is_manufacturer %}manufacturer: {{ company.id }},{% endif %} }, reload: true, secondary: [ { field: 'part', - label: 'New Part', - title: 'Create New Part', + label: '{% trans "New Part" %}', + title: '{% trans "Create new Part" %}', url: "{% url 'part-create' %}" }, + { + field: 'supplier', + label: "{% trans 'New Supplier' %}", + title: "{% trans 'Create new Supplier' %}", + url: "{% url 'supplier-create' %}", + }, + { + field: 'manufacturer', + label: '{% trans "New Manufacturer" %}', + title: '{% trans "Create new Manufacturer" %}', + url: "{% url 'manufacturer-create' %}", + }, ] }); }); diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index d1fc9b643f..ae88629505 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -273,10 +273,6 @@ class SupplierPartCreate(AjaxCreateView): Hide some fields if they are not appropriate in context """ form = super(AjaxCreateView, self).get_form() - - if form.initial.get('supplier', None): - # Hide the supplier field - form.fields['supplier'].widget = HiddenInput() if form.initial.get('part', None): # Hide the part field @@ -292,20 +288,27 @@ class SupplierPartCreate(AjaxCreateView): """ initials = super(SupplierPartCreate, self).get_initial().copy() + manufacturer_id = self.get_param('manufacturer') supplier_id = self.get_param('supplier') part_id = self.get_param('part') if supplier_id: try: initials['supplier'] = Company.objects.get(pk=supplier_id) - except Company.DoesNotExist: - initials['supplier'] = None + except (ValueError, Company.DoesNotExist): + pass + + if manufacturer_id: + try: + initials['manufacturer'] = Company.objects.get(pk=manufacturer_id) + except (ValueError, Company.DoesNotExist): + pass if part_id: try: initials['part'] = Part.objects.get(pk=part_id) - except Part.DoesNotExist: - initials['part'] = None + except (ValueError, Part.DoesNotExist): + pass return initials From fb8c0e518098840dbb339e976dc44c6a49ce6885 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 13 Apr 2020 22:53:14 +1000 Subject: [PATCH 06/37] Fix buggy migration - Need to use raw SQL queries as the database model does not match the python model --- .../migrations/0019_auto_20200413_0642.py | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/InvenTree/company/migrations/0019_auto_20200413_0642.py b/InvenTree/company/migrations/0019_auto_20200413_0642.py index 2c120b30b4..e961d5fcbf 100644 --- a/InvenTree/company/migrations/0019_auto_20200413_0642.py +++ b/InvenTree/company/migrations/0019_auto_20200413_0642.py @@ -3,7 +3,7 @@ import os from rapidfuzz import fuzz -from django.db import migrations +from django.db import migrations, connection from company.models import Company, SupplierPart from django.db.utils import OperationalError, ProgrammingError @@ -54,6 +54,29 @@ def associate_manufacturers(apps, schema_editor): It uses fuzzy pattern matching to help the user out as much as possible. """ + + def get_manufacturer_name(part_id): + """ + THIS IS CRITICAL! + + Once the pythonic representation of the model has removed the 'manufacturer_name' field, + it is NOT ACCESSIBLE by calling SupplierPart.manufacturer_name. + + However, as long as the migrations are applied in order, then the table DOES have a field called 'manufacturer_name'. + + So, we just need to request it using dirty SQL. + """ + + query = "SELECT manufacturer_name from part_supplierpart where id={ID};".format(ID=part_id) + + cursor = connection.cursor() + response = cursor.execute(query) + row = response.fetchone() + + if len(row) > 0: + return row[0] + return '' + # Exit if there are no SupplierPart objects # This crucial otherwise the unit test suite fails! @@ -140,7 +163,7 @@ def associate_manufacturers(apps, schema_editor): def map_part_to_manufacturer(part, idx, total): - name = str(part.manufacturer_name) + name = get_manufacturer_name(part.id) # Skip empty names if not name or len(name) == 0: @@ -209,6 +232,8 @@ def associate_manufacturers(apps, schema_editor): print(" -> Linked '{n}' to manufacturer '{m}'".format(n=name, m=company_name)) return + else: + print("Please select a valid option") except ValueError: # User has typed in a custom name! From 3b08b962c159b90b74af196cdf0a4513f7aaf673 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 13 Apr 2020 23:19:23 +1000 Subject: [PATCH 07/37] Fix order of operations --- .../migrations/0019_auto_20200413_0642.py | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/InvenTree/company/migrations/0019_auto_20200413_0642.py b/InvenTree/company/migrations/0019_auto_20200413_0642.py index e961d5fcbf..4c49a5e07c 100644 --- a/InvenTree/company/migrations/0019_auto_20200413_0642.py +++ b/InvenTree/company/migrations/0019_auto_20200413_0642.py @@ -93,23 +93,19 @@ def associate_manufacturers(apps, schema_editor): for company in Company.objects.all(): companies[company.name] = company - # List of parts which will need saving - parts = [] - - def link_part(part, name): """ Attempt to link Part to an existing Company """ # Matches a company name directly if name in companies.keys(): - print(" -> '{n}' maps to existing manufacturer".format(n=name)) + print(" - Part[{pk}]: '{n}' maps to existing manufacturer".format(pk=part.pk, n=name)) part.manufacturer = companies[name] part.save() return True # Have we already mapped this if name in links.keys(): - print(" -> Mapped '{n}' -> '{c}'".format(n=name, c=links[name].name)) + print(" - Part[{pk}]: Mapped '{n}' - '{c}'".format(pk=part.pk, n=name, c=links[name].name)) part.manufacturer = links[name] part.save() return True @@ -123,23 +119,22 @@ def associate_manufacturers(apps, schema_editor): company = Company(name=company_name, description=company_name, is_manufacturer=True) company.is_manufacturer = True + + # Save the company BEFORE we associate the part, otherwise the PK does not exist + company.save() # Map both names to the same company links[input_name] = company links[company_name] = company companies[company_name] = company - - # Save the company BEFORE we associate the part, otherwise the PK does not exist - company.save() + print(" - Part[{pk}]: Created new manufacturer: '{name}'".format(pk=part.pk, name=company_name)) + # Save the manufacturer reference link part.manufacturer = company part.save() - print(" -> Created new manufacturer: '{name}'".format(name=company_name)) - - def find_matches(text, threshold=65): """ Attempt to match a 'name' to an existing Company. @@ -167,6 +162,7 @@ def associate_manufacturers(apps, schema_editor): # Skip empty names if not name or len(name) == 0: + print(" - Part[{pk}]: No manufacturer_name provided, skipping".format(pk=part.pk)) return # Can be linked to an existing manufacturer @@ -180,7 +176,7 @@ def associate_manufacturers(apps, schema_editor): # Present a list of options print("----------------------------------") - print("Checking part {idx} of {total}".format(idx=idx+1, total=total)) + print("Checking part [{pk}] ({idx} of {total})".format(pk=part.pk, idx=idx+1, total=total)) print("Manufacturer name: '{n}'".format(n=name)) print("----------------------------------") print("Select an option from the list below:") @@ -193,9 +189,8 @@ def associate_manufacturers(apps, schema_editor): print("") print("OR - Type a new custom manufacturer name") - - while (1): + while True: response = str(input("> ")).strip() # Attempt to parse user response as an integer @@ -208,7 +203,7 @@ def associate_manufacturers(apps, schema_editor): create_manufacturer(part, name, name) return - # Options 1) -> n) select an existing manufacturer + # Options 1) - n) select an existing manufacturer else: n = n - 1 @@ -229,7 +224,7 @@ def associate_manufacturers(apps, schema_editor): links[name] = company links[company_name] = company - print(" -> Linked '{n}' to manufacturer '{m}'".format(n=name, m=company_name)) + print(" - Part[{pk}]: Linked '{n}' to manufacturer '{m}'".format(pk=part.pk, n=name, m=company_name)) return else: @@ -281,11 +276,10 @@ def associate_manufacturers(apps, schema_editor): for idx, part in enumerate(SupplierPart.objects.all()): if part.manufacturer is not None: - print(" -> Part '{p}' already has a manufacturer associated (skipping)".format(p=part)) + print(" - Part '{p}' already has a manufacturer associated (skipping)".format(p=part)) continue map_part_to_manufacturer(part, idx, part_count) - parts.append(part) print("Done!") From 9ff5032020a3c07996386e0139b6c455ed1f28af Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 13 Apr 2020 23:39:56 +1000 Subject: [PATCH 08/37] 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 653d502a734b8f4df8706fe5f63bc85e5130cac9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 13 Apr 2020 23:58:10 +1000 Subject: [PATCH 09/37] Fix the manufacturer migration so it reverses properly --- .../migrations/0019_auto_20200413_0642.py | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/InvenTree/company/migrations/0019_auto_20200413_0642.py b/InvenTree/company/migrations/0019_auto_20200413_0642.py index 4c49a5e07c..f683ee783a 100644 --- a/InvenTree/company/migrations/0019_auto_20200413_0642.py +++ b/InvenTree/company/migrations/0019_auto_20200413_0642.py @@ -29,17 +29,41 @@ def reverse_association(apps, schema_editor): print("Reversing migration for manufacturer association") - try: - for part in SupplierPart.objects.all(): - if part.manufacturer is not None: - part.manufacturer_name = part.manufacturer.name - - part.save() + for part in SupplierPart.objects.all(): - except (OperationalError, ProgrammingError): - # An exception might be called if the database is empty - pass + print("Checking part [{pk}]:".format(pk=part.pk)) + cursor = connection.cursor() + + # Grab the manufacturer ID from the part + response = cursor.execute('SELECT manufacturer_id FROM part_supplierpart WHERE id={ID};'.format(ID=part.id)) + + manufacturer_id = None + + row = response.fetchone() + + if len(row) > 0: + try: + manufacturer_id = int(row[0]) + except (TypeError, ValueError): + pass + + if manufacturer_id is None: + print(" - Manufacturer ID not set: Skipping") + continue + + print(" - Manufacturer ID: [{id}]".format(id=manufacturer_id)) + + # Now extract the "name" for the manufacturer + response = cursor.execute('SELECT name from company_company where id={ID};'.format(ID=manufacturer_id)) + + row = response.fetchone() + + name = row[0] + + print(" - Manufacturer name: '{name}'".format(name=name)) + + response = cursor.execute("UPDATE part_supplierpart SET manufacturer_name='{name}' WHERE id={ID};".format(name=name, ID=part.id)) def associate_manufacturers(apps, schema_editor): """ From b286a5e30cbf392ec79d955ac86c0cac12010fae Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 01:17:44 +1000 Subject: [PATCH 10/37] 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 11/37] 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 12/37] 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 7c9eb90bead28f5d1248889393e9fa4c9d4fc4bb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 08:25:10 +1000 Subject: [PATCH 13/37] URL fix --- InvenTree/part/templatetags/inventree_extras.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index e68e9c23dc..ab3b0694ac 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -70,7 +70,7 @@ def inventree_commit_date(*args, **kwargs): @register.simple_tag() def inventree_github_url(*args, **kwargs): """ Return URL for InvenTree github site """ - return "https://github.com/InvenTree" + return "https://github.com/InvenTree/InvenTree/" @register.simple_tag() From 70589b06e1f1c6fa3618c1f6f1fa8daba2862982 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 19:27:43 +1000 Subject: [PATCH 14/37] 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 15/37] 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 16/37] 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 17/37] 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 18/37] 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 19/37] 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 20/37] 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 21/37] 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 22/37] 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 23/37] 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 24/37] 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 25/37] 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 26/37] 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 27/37] 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 28/37] 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 29/37] 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 30/37] 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 31/37] 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 32/37] 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 From 4594f1e2b890da35db56bcdd91e2121248309f38 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 16 Apr 2020 09:56:32 +1000 Subject: [PATCH 33/37] Update requirements documentation - Add python3-dev and g++ - Add wheel to PIP requirements file --- docs/start.rst | 8 +++++++- requirements.txt | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/start.rst b/docs/start.rst index 1b389d7e5b..25cc39945a 100644 --- a/docs/start.rst +++ b/docs/start.rst @@ -17,10 +17,16 @@ Requirements To install InvenTree you will need the following system components installed: * python3 +* python3-dev * python3-pip (pip3) +* g++ * make -Each of these programs need to be installed (e.g. using apt or similar) before running the ``make install`` script. +Each of these programs need to be installed (e.g. using apt or similar) before running the ``make install`` scriptm e.g. + +``` +sudo apt-get install python3 python3-dev python3-pip g++ make +``` Virtual Environment ------------------- diff --git a/requirements.txt b/requirements.txt index ff65664282..4eace551ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -Django==2.2.10 # Django package +wheel>=0.34.2 # Wheel +Django==2.2.10 # Django package pillow==6.2.0 # Image manipulation djangorestframework==3.10.3 # DRF framework django-cors-headers==3.2.0 # CORS headers extension for DRF From 539b000460304de51c76b753b132cf20ffdc9555 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 16 Apr 2020 21:02:14 +1000 Subject: [PATCH 34/37] Update start.rst typo fix --- docs/start.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/start.rst b/docs/start.rst index 25cc39945a..45a32ffb0b 100644 --- a/docs/start.rst +++ b/docs/start.rst @@ -22,7 +22,7 @@ To install InvenTree you will need the following system components installed: * g++ * make -Each of these programs need to be installed (e.g. using apt or similar) before running the ``make install`` scriptm e.g. +Each of these programs need to be installed (e.g. using apt or similar) before running the ``make install`` script: ``` sudo apt-get install python3 python3-dev python3-pip g++ make @@ -121,4 +121,4 @@ Other shorthand functions are provided for the development and testing process: * ``make coverage`` - Run all unit tests and generate code coverage report * ``make style`` - Check Python codebase against PEP coding standards (using Flake) * ``make docreqs`` - Install the packages required to generate documentation -* ``make docs`` - Generate this documentation \ No newline at end of file +* ``make docs`` - Generate this documentation From 7ab58f683fcce9d75f11af3a8c7208bb1b76e6e9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 16 Apr 2020 21:33:35 +1000 Subject: [PATCH 35/37] Fix search result tables --- InvenTree/part/templates/part/category.html | 47 ++++++++++--------- InvenTree/templates/InvenTree/search.html | 4 +- .../templates/InvenTree/search_parts.html | 6 +++ 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 63a60bd71e..9382259cce 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -137,29 +137,30 @@ $("#cat-create").click(function() { launchModalForm( - "{% url 'category-create' %}", - { - follow: true, - {% if category %} - data: { - category: {{ category.id }} - }, - {% endif %} - secondary: [ - { - field: 'default_location', - label: 'New Location', - title: 'Create new location', - url: "{% url 'stock-location-create' %}", - }, - { - field: 'parent', - label: 'New Category', - title: 'Create new category', - url: "{% url 'category-create' %}", - }, - ] - }); + "{% url 'category-create' %}", + { + follow: true, + {% if category %} + data: { + category: {{ category.id }} + }, + {% endif %} + secondary: [ + { + field: 'default_location', + label: 'New Location', + title: 'Create new location', + url: "{% url 'stock-location-create' %}", + }, + { + field: 'parent', + label: 'New Category', + title: 'Create new category', + url: "{% url 'category-create' %}", + }, + ] + } + ); }) $("#part-export").click(function() { diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html index e8cda96809..99b704c626 100644 --- a/InvenTree/templates/InvenTree/search.html +++ b/InvenTree/templates/InvenTree/search.html @@ -127,7 +127,7 @@ InvenTree | {% trans "Search Results" %} loadPartTable("#part-results-table", "{% url 'api-part-list' %}", { - query: { + params: { search: "{{ query }}", }, allowInactive: true, @@ -137,7 +137,7 @@ InvenTree | {% trans "Search Results" %} loadCompanyTable('#company-results-table', "{% url 'api-company-list' %}", { params: { - serach: "{{ query }}", + search: "{{ query }}", } }); diff --git a/InvenTree/templates/InvenTree/search_parts.html b/InvenTree/templates/InvenTree/search_parts.html index 69c5c29051..f06155354d 100644 --- a/InvenTree/templates/InvenTree/search_parts.html +++ b/InvenTree/templates/InvenTree/search_parts.html @@ -9,6 +9,12 @@ {% endblock %} {% block collapse_content %} +
+
+
+
+
+
{% 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 }}
{% endblock %} \ No newline at end of file From 206d67337f033be1cc5a8fcf9a2258ba805b01bd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 16 Apr 2020 21:41:45 +1000 Subject: [PATCH 36/37] Option to disable filters for part table --- InvenTree/InvenTree/static/script/inventree/part.js | 8 ++++++-- InvenTree/templates/InvenTree/search.html | 7 +++++-- InvenTree/templates/InvenTree/search_parts.html | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/part.js b/InvenTree/InvenTree/static/script/inventree/part.js index 05328dbbd8..7954caa012 100644 --- a/InvenTree/InvenTree/static/script/inventree/part.js +++ b/InvenTree/InvenTree/static/script/inventree/part.js @@ -81,15 +81,19 @@ function loadPartTable(table, url, options={}) { * - table: HTML reference to the table * - url: Base URL for API query * - options: object containing following (optional) fields - * allowInactive: If true, allow display of inactive parts * checkbox: Show the checkbox column * query: extra query params for API request * buttons: If provided, link buttons to selection status of this table + * disableFilters: If true, disable custom filters */ var params = options.params || {}; - var filters = loadTableFilters("parts"); + var filters = {}; + + if (!options.disableFilters) { + filters = loadTableFilters("parts"); + } for (var key in params) { filters[key] = params[key]; diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html index 99b704c626..76fd62b697 100644 --- a/InvenTree/templates/InvenTree/search.html +++ b/InvenTree/templates/InvenTree/search.html @@ -104,6 +104,7 @@ InvenTree | {% trans "Search Results" %} ], }); + $("#location-results-table").inventreeTable({ url: "{% url 'api-location-list' %}", queryParams: { @@ -124,16 +125,18 @@ InvenTree | {% trans "Search Results" %} ], }); + loadPartTable("#part-results-table", "{% url 'api-part-list' %}", { params: { search: "{{ query }}", }, - allowInactive: true, checkbox: false, + disableFilters: true, } ); + loadCompanyTable('#company-results-table', "{% url 'api-company-list' %}", { params: { @@ -153,5 +156,5 @@ InvenTree | {% trans "Search Results" %} }, } ); - + {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/search_parts.html b/InvenTree/templates/InvenTree/search_parts.html index f06155354d..ca75b096c2 100644 --- a/InvenTree/templates/InvenTree/search_parts.html +++ b/InvenTree/templates/InvenTree/search_parts.html @@ -11,6 +11,7 @@ {% block collapse_content %}
+
From 0ee53758b462580479afaa973f168bbab0bb1c75 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 16 Apr 2020 21:43:21 +1000 Subject: [PATCH 37/37] Add same optio for stock table --- InvenTree/InvenTree/static/script/inventree/stock.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/static/script/inventree/stock.js b/InvenTree/InvenTree/static/script/inventree/stock.js index bf0ddbef5e..d68d0946a2 100644 --- a/InvenTree/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/InvenTree/static/script/inventree/stock.js @@ -41,6 +41,7 @@ function loadStockTable(table, options) { * groupByField - Column for grouping stock items * buttons - Which buttons to link to stock selection callbacks * filterList -
    element where filters are displayed + * disableFilters: If true, disable custom filters */ // List of user-params which override the default filters @@ -48,7 +49,11 @@ function loadStockTable(table, options) { var filterListElement = options.filterList || "#filter-list-stock"; - var filters = loadTableFilters("stock"); + var filters = {}; + + if (!options.disableFilters) { + filters = loadTableFilters("stock"); + } var original = {};