diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py
index 3489056865..2fc85ef653 100644
--- a/InvenTree/InvenTree/api.py
+++ b/InvenTree/InvenTree/api.py
@@ -18,7 +18,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from .views import AjaxView
-from .version import inventreeVersion, inventreeInstanceName
+from .version import inventreeVersion, inventreeApiVersion, inventreeInstanceName
from plugins import plugins as inventree_plugins
@@ -43,6 +43,7 @@ class InfoView(AjaxView):
'server': 'InvenTree',
'version': inventreeVersion(),
'instance': inventreeInstanceName(),
+ 'apiVersion': inventreeApiVersion(),
}
return JsonResponse(data)
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index 5dbfb845bc..3abb99010f 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -275,12 +275,13 @@ REST_FRAMEWORK = {
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
),
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
'rest_framework.permissions.DjangoModelPermissions',
'InvenTree.permissions.RolePermission',
),
- 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'
+ 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
}
WSGI_APPLICATION = 'InvenTree.wsgi.application'
diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index c51398e182..1ffab1be96 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -9,6 +9,9 @@ import common.models
INVENTREE_SW_VERSION = "0.1.7 pre"
+# Increment this number whenever there is a significant change to the API that any clients need to know about
+INVENTREE_API_VERSION = 2
+
def inventreeInstanceName():
""" Returns the InstanceName settings for the current database """
@@ -20,6 +23,10 @@ def inventreeVersion():
return INVENTREE_SW_VERSION
+def inventreeApiVersion():
+ return INVENTREE_API_VERSION
+
+
def inventreeDjangoVersion():
""" Return the version of Django library """
return django.get_version()
diff --git a/InvenTree/company/templates/company/index.html b/InvenTree/company/templates/company/index.html
index ae3e191bc1..ca2fa619d9 100644
--- a/InvenTree/company/templates/company/index.html
+++ b/InvenTree/company/templates/company/index.html
@@ -22,7 +22,7 @@ InvenTree | {% trans "Supplier List" %}
{% endif %}
-
+
{% endblock %}
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 9a0e3a2a68..a245f9d67c 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -397,11 +397,11 @@ class PartList(generics.ListCreateAPIView):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
+
if page is not None:
serializer = self.get_serializer(page, many=True)
- return self.get_paginated_response(serializer.data)
-
- serializer = self.get_serializer(queryset, many=True)
+ else:
+ serializer = self.get_serializer(queryset, many=True)
data = serializer.data
@@ -445,7 +445,9 @@ class PartList(generics.ListCreateAPIView):
a) For HTTP requests (e.g. via the browseable API) return a DRF response
b) For AJAX requests, simply return a JSON rendered response.
"""
- if request.is_ajax():
+ if page is not None:
+ return self.get_paginated_response(data)
+ elif request.is_ajax():
return JsonResponse(data, safe=False)
else:
return Response(data)
@@ -641,15 +643,18 @@ class PartList(generics.ListCreateAPIView):
queryset = queryset.filter(pk__in=parts_need_stock)
- # Limit number of results
- limit = params.get('limit', None)
+ # Optionally limit the maximum number of returned results
+ # e.g. for displaying "recent part" list
+ max_results = params.get('max_results', None)
- if limit is not None:
+ if max_results is not None:
try:
- limit = int(limit)
- if limit > 0:
- queryset = queryset[:limit]
- except ValueError:
+ max_results = int(max_results)
+
+ if max_results > 0:
+ queryset = queryset[:max_results]
+
+ except (ValueError):
pass
return queryset
@@ -674,6 +679,8 @@ class PartList(generics.ListCreateAPIView):
ordering_fields = [
'name',
'creation_date',
+ 'IPN',
+ 'in_stock',
]
# Default ordering
@@ -685,6 +692,7 @@ class PartList(generics.ListCreateAPIView):
'IPN',
'revision',
'keywords',
+ 'category__name',
]
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index d917b6ebb2..faadf26c15 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -235,6 +235,21 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
+ def test_paginate(self):
+ """
+ Test pagination of the Part list API
+ """
+
+ for n in [1, 5, 10]:
+ response = self.get(reverse('api-part-list'), {'limit': n})
+
+ data = response.data
+
+ self.assertIn('count', data)
+ self.assertIn('results', data)
+
+ self.assertEqual(len(data['results']), n)
+
class PartAPIAggregationTest(InvenTreeAPITestCase):
"""
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index fe669da5b9..15edfed066 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -381,7 +381,12 @@ class StockList(generics.ListCreateAPIView):
queryset = self.filter_queryset(self.get_queryset())
- serializer = self.get_serializer(queryset, many=True)
+ page = self.paginate_queryset(queryset)
+
+ if page is not None:
+ serializer = self.get_serializer(page, many=True)
+ else:
+ serializer = self.get_serializer(queryset, many=True)
data = serializer.data
@@ -465,7 +470,9 @@ class StockList(generics.ListCreateAPIView):
Note: b) is about 100x quicker than a), because the DRF framework adds a lot of cruft
"""
- if request.is_ajax():
+ if page is not None:
+ return self.get_paginated_response(data)
+ elif request.is_ajax():
return JsonResponse(data, safe=False)
else:
return Response(data)
@@ -806,16 +813,15 @@ class StockList(generics.ListCreateAPIView):
print("After error:", str(updated_after))
pass
- # Limit number of results
- limit = params.get('limit', None)
+ # Optionally, limit the maximum number of returned results
+ max_results = params.get('max_results', None)
- if limit is not None:
+ if max_results is not None:
try:
- limit = int(limit)
-
- if limit > 0:
- queryset = queryset[:limit]
+ max_results = int(max_results)
+ if max_results > 0:
+ queryset = queryset[:max_results]
except (ValueError):
pass
@@ -839,9 +845,12 @@ class StockList(generics.ListCreateAPIView):
ordering_fields = [
'part__name',
+ 'part__IPN',
'updated',
'stocktake_date',
'expiry_date',
+ 'quantity',
+ 'status',
]
ordering = ['part__name']
@@ -851,7 +860,8 @@ class StockList(generics.ListCreateAPIView):
'batch',
'part__name',
'part__IPN',
- 'part__description'
+ 'part__description',
+ 'location__name',
]
diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html
index 6f13de30a3..a1101f1bdb 100644
--- a/InvenTree/stock/templates/stock/item.html
+++ b/InvenTree/stock/templates/stock/item.html
@@ -52,12 +52,10 @@
loadStockTrackingTable($("#track-table"), {
- params: function(p) {
- return {
- ordering: '-date',
- item: {{ item.pk }},
- user_detail: true,
- };
+ params: {
+ ordering: '-date',
+ item: {{ item.pk }},
+ user_detail: true,
},
url: "{% url 'api-stock-track' %}",
});
diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py
index 17cab7ffe3..ae0d6fd862 100644
--- a/InvenTree/stock/test_api.py
+++ b/InvenTree/stock/test_api.py
@@ -244,6 +244,19 @@ class StockItemListTest(StockAPITestCase):
response = self.get_stock(expired=0)
self.assertEqual(len(response), 16)
+ def test_paginate(self):
+ """
+ Test that we can paginate results correctly
+ """
+
+ for n in [1, 5, 10]:
+ response = self.get_stock(limit=n)
+
+ self.assertIn('count', response)
+ self.assertIn('results', response)
+
+ self.assertEqual(len(response['results']), n)
+
class StockItemTest(StockAPITestCase):
"""
diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html
index cb2cda0a30..2023227bce 100644
--- a/InvenTree/templates/InvenTree/index.html
+++ b/InvenTree/templates/InvenTree/index.html
@@ -102,7 +102,7 @@ addHeaderAction('bom-validation', '{% trans "BOM Waiting Validation" %}', 'fa-ti
loadSimplePartTable("#table-latest-parts", "{% url 'api-part-list' %}", {
params: {
ordering: "-creation_date",
- limit: {% settings_value "PART_RECENT_COUNT" %},
+ max_results: {% settings_value "PART_RECENT_COUNT" %},
},
name: 'latest_parts',
});
@@ -132,7 +132,7 @@ addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa
loadStockTable($('#table-recently-updated-stock'), {
params: {
ordering: "-updated",
- limit: {% settings_value "STOCK_RECENT_COUNT" %},
+ max_results: {% settings_value "STOCK_RECENT_COUNT" %},
},
name: 'recently-updated-stock',
grouping: false,
diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js
index 01d5fcdef0..45ebd133d1 100644
--- a/InvenTree/templates/js/build.js
+++ b/InvenTree/templates/js/build.js
@@ -630,6 +630,7 @@ function loadBuildTable(table, options) {
url: options.url,
queryParams: filters,
groupBy: false,
+ sidePagination: 'server',
name: 'builds',
original: params,
columns: [
diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js
index 388a79668b..b497609db1 100644
--- a/InvenTree/templates/js/company.js
+++ b/InvenTree/templates/js/company.js
@@ -93,6 +93,7 @@ function loadCompanyTable(table, url, options={}) {
method: 'get',
queryParams: filters,
groupBy: false,
+ sidePagination: 'server',
formatNoMatches: function() { return "{% trans "No company information found" %}"; },
showColumns: true,
name: options.pagetype || 'company',
diff --git a/InvenTree/templates/js/order.js b/InvenTree/templates/js/order.js
index 53063cd709..c4ca4acd08 100644
--- a/InvenTree/templates/js/order.js
+++ b/InvenTree/templates/js/order.js
@@ -131,6 +131,7 @@ function loadPurchaseOrderTable(table, options) {
queryParams: filters,
name: 'purchaseorder',
groupBy: false,
+ sidePagination: 'server',
original: options.params,
formatNoMatches: function() { return '{% trans "No purchase orders found" %}'; },
columns: [
@@ -225,6 +226,7 @@ function loadSalesOrderTable(table, options) {
queryParams: filters,
name: 'salesorder',
groupBy: false,
+ sidePagination: 'server',
original: options.params,
formatNoMatches: function() { return '{% trans "No sales orders found" %}'; },
columns: [
diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js
index 9f770f452e..cefc2af8a7 100644
--- a/InvenTree/templates/js/part.js
+++ b/InvenTree/templates/js/part.js
@@ -366,7 +366,6 @@ function loadPartTable(table, url, options={}) {
});
columns.push({
- sortable: true,
field: 'description',
title: '{% trans "Description" %}',
formatter: function(value, row, index, field) {
@@ -442,12 +441,13 @@ function loadPartTable(table, url, options={}) {
$(table).inventreeTable({
url: url,
- sortName: 'name',
method: 'get',
queryParams: filters,
groupBy: false,
name: options.name || 'part',
original: params,
+ sidePagination: 'server',
+ pagination: 'true',
formatNoMatches: function() { return '{% trans "No parts found" %}'; },
columns: columns,
showColumns: true,
diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js
index b115b8171c..bfb539c3c5 100644
--- a/InvenTree/templates/js/stock.js
+++ b/InvenTree/templates/js/stock.js
@@ -325,6 +325,12 @@ function loadStockTable(table, options) {
grouping = options.grouping;
}
+ // Explicitly disable part grouping functionality
+ // Might be able to add this in later on,
+ // but there is a bug which makes this crash if paginating on the server side.
+ // Ref: https://github.com/wenzhixin/bootstrap-table/issues/3250
+ grouping = false;
+
table.inventreeTable({
method: 'get',
formatNoMatches: function() {
@@ -332,7 +338,7 @@ function loadStockTable(table, options) {
},
url: options.url || "{% url 'api-stock-list' %}",
queryParams: filters,
- customSort: customGroupSorter,
+ sidePagination: 'server',
name: 'stock',
original: original,
showColumns: true,
@@ -516,6 +522,7 @@ function loadStockTable(table, options) {
{
field: 'part_detail.full_name',
title: '{% trans "Part" %}',
+ sortName: 'part__name',
sortable: true,
switchable: false,
formatter: function(value, row, index, field) {
@@ -534,6 +541,7 @@ function loadStockTable(table, options) {
{
field: 'part_detail.IPN',
title: 'IPN',
+ sortName: 'part__IPN',
sortable: true,
formatter: function(value, row, index, field) {
return row.part_detail.IPN;
@@ -542,7 +550,6 @@ function loadStockTable(table, options) {
{
field: 'part_detail.description',
title: '{% trans "Description" %}',
- sortable: true,
formatter: function(value, row, index, field) {
return row.part_detail.description;
}
@@ -654,8 +661,6 @@ function loadStockTable(table, options) {
{
field: 'packaging',
title: '{% trans "Packaging" %}',
- sortable: true,
- searchable: true,
},
{
field: 'notes',
@@ -927,7 +932,6 @@ function loadStockTrackingTable(table, options) {
cols.push({
field: 'title',
title: '{% trans "Description" %}',
- sortable: true,
formatter: function(value, row, index, field) {
var html = "" + value + "";
@@ -952,7 +956,6 @@ function loadStockTrackingTable(table, options) {
});
cols.push({
- sortable: true,
field: 'user',
title: '{% trans "User" %}',
formatter: function(value, row, index, field) {
diff --git a/InvenTree/templates/js/table_filters.js b/InvenTree/templates/js/table_filters.js
index 39224c4ffe..fb76d2731b 100644
--- a/InvenTree/templates/js/table_filters.js
+++ b/InvenTree/templates/js/table_filters.js
@@ -1,5 +1,6 @@
{% load i18n %}
{% load status_codes %}
+{% load inventree_extras %}
{% include "status_codes.html" with label='stock' options=StockStatus.list %}
{% include "status_codes.html" with label='build' options=BuildStatus.list %}
@@ -110,6 +111,8 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Depleted" %}',
description: '{% trans "Show stock items which are depleted" %}',
},
+ {% settings_value "STOCK_ENABLE_EXPIRY" as expiry %}
+ {% if expiry %}
expired: {
type: 'bool',
title: '{% trans "Expired" %}',
@@ -120,6 +123,7 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Stale" %}',
description: '{% trans "Show stock which is close to expiring" %}',
},
+ {% endif %}
in_stock: {
type: 'bool',
title: '{% trans "In Stock" %}',
diff --git a/InvenTree/templates/js/tables.js b/InvenTree/templates/js/tables.js
index ceb38690a8..f09c683bff 100644
--- a/InvenTree/templates/js/tables.js
+++ b/InvenTree/templates/js/tables.js
@@ -93,7 +93,14 @@ function reloadTable(table, filters) {
}
}
- options.queryParams = params;
+ options.queryParams = function(tableParams) {
+
+ for (key in params) {
+ tableParams[key] = params[key];
+ }
+
+ return tableParams;
+ }
table.bootstrapTable('refreshOptions', options);
table.bootstrapTable('refresh');
@@ -126,9 +133,45 @@ $.fn.inventreeTable = function(options) {
var varName = tableName + '-pagesize';
+ // Pagingation options (can be server-side or client-side as specified by the caller)
options.pagination = true;
+ options.paginationVAlign = 'both';
options.pageSize = inventreeLoad(varName, 25);
options.pageList = [25, 50, 100, 250, 'all'];
+ options.totalField = 'count';
+ options.dataField = 'results';
+
+ // Extract query params
+ var filters = options.queryParams || options.filters || {};
+
+ options.queryParams = function(params) {
+ for (var key in filters) {
+ params[key] = filters[key];
+ }
+
+ // Override the way that we ask the server to sort results
+ // It seems bootstrap-table does not offer a "native" way to do this...
+ if ('sort' in params) {
+ var order = params['order'];
+
+ var ordering = params['sort'] || null;
+
+ if (ordering) {
+
+ if (order == 'desc') {
+ ordering = `-${ordering}`;
+ }
+
+ params['ordering'] = ordering;
+ }
+
+ delete params['sort'];
+ delete params['order'];
+
+ }
+
+ return params;
+ }
options.rememberOrder = true;