mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
a604c828e4
@ -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,11 +43,29 @@ class InfoView(AjaxView):
|
||||
'server': 'InvenTree',
|
||||
'version': inventreeVersion(),
|
||||
'instance': inventreeInstanceName(),
|
||||
'apiVersion': inventreeApiVersion(),
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
class NotFoundView(AjaxView):
|
||||
"""
|
||||
Simple JSON view when accessing an invalid API view.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
data = {
|
||||
'details': _('API endpoint not found'),
|
||||
'url': request.build_absolute_uri(),
|
||||
}
|
||||
|
||||
return JsonResponse(data, status=404)
|
||||
|
||||
|
||||
class AttachmentMixin:
|
||||
"""
|
||||
Mixin for creating attachment objects,
|
||||
|
@ -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'
|
||||
|
@ -725,6 +725,7 @@ input[type="submit"] {
|
||||
top: 70px;
|
||||
position: sticky;
|
||||
font-size: 115%;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.sidenav-right svg {
|
||||
|
@ -67,79 +67,91 @@ function loadTree(url, tree, options={}) {
|
||||
});
|
||||
}
|
||||
|
||||
function openSideNav(navId) {
|
||||
// document.getElementById("sidenav").style.display = "block";
|
||||
// document.getElementById("sidenav").style.width = "250px";
|
||||
|
||||
if (!navId) {
|
||||
navId = '#sidenav-left';
|
||||
/**
|
||||
* Initialize navigation tree display
|
||||
*/
|
||||
function initNavTree(options) {
|
||||
|
||||
var resize = true;
|
||||
|
||||
if ('resize' in options) {
|
||||
resize = options.resize;
|
||||
}
|
||||
|
||||
sessionStorage.setItem('inventree-sidenav-state', 'open');
|
||||
var label = options.label || 'nav';
|
||||
|
||||
$(navId).animate({
|
||||
width: '250px',
|
||||
'min-width': '200px',
|
||||
display: 'block'
|
||||
}, 50);
|
||||
var stateLabel = `${label}-tree-state`;
|
||||
var widthLabel = `${label}-tree-width`;
|
||||
|
||||
var treeId = options.treeId || '#sidenav-left';
|
||||
var toggleId = options.toggleId;
|
||||
|
||||
}
|
||||
|
||||
function closeSideNav(navId) {
|
||||
|
||||
if (!navId) {
|
||||
navId = '#sidenav-left';
|
||||
}
|
||||
|
||||
sessionStorage.setItem('inventree-sidenav-state', 'closed');
|
||||
|
||||
$(navId).animate({
|
||||
// Initially hide the tree
|
||||
$(treeId).animate({
|
||||
width: '0px',
|
||||
'min-width': '0px',
|
||||
display: 'none',
|
||||
}, 50);
|
||||
}, 0, function() {
|
||||
|
||||
//document.getElementById("sidenav").style.display = "none";
|
||||
//document.getElementById("sidenav").style.width = "0";
|
||||
//document.getElementById("inventree-content").style.marginLeft = "0px";
|
||||
if (resize) {
|
||||
$(treeId).resizable({
|
||||
minWidth: '0px',
|
||||
maxWidth: '500px',
|
||||
handles: 'e, se',
|
||||
grid: [5, 5],
|
||||
stop: function(event, ui) {
|
||||
var width = Math.round(ui.element.width());
|
||||
|
||||
}
|
||||
if (width < 75) {
|
||||
$(treeId).animate({
|
||||
width: '0px'
|
||||
}, 50);
|
||||
|
||||
function toggleSideNav(nav) {
|
||||
if ($(nav).width() <= 0) {
|
||||
openSideNav(nav);
|
||||
}
|
||||
else {
|
||||
closeSideNav(nav);
|
||||
}
|
||||
}
|
||||
sessionStorage.setItem(stateLabel, 'closed');
|
||||
} else {
|
||||
sessionStorage.setItem(stateLabel, 'open');
|
||||
sessionStorage.setItem(widthLabel, `${width}px`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initSideNav(navId) {
|
||||
var state = sessionStorage.getItem(stateLabel);
|
||||
var width = sessionStorage.getItem(widthLabel) || '300px';
|
||||
|
||||
// Make it resizable
|
||||
if (state && state == 'open') {
|
||||
|
||||
if (!navId) {
|
||||
navId = '#sidenav-left';
|
||||
}
|
||||
|
||||
$(navId).resizable({
|
||||
minWidth: '100px',
|
||||
maxWidth: '500px',
|
||||
stop: function(event, ui) {
|
||||
console.log(ui.element.width());
|
||||
//console.log(ui.size.width);
|
||||
$(treeId).animate({
|
||||
width: width,
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
|
||||
if (sessionStorage.getItem("inventree-sidenav-state") && sessionStorage.getItem('inventree-sidenav-state') == 'open') {
|
||||
openSideNav(navId);
|
||||
}
|
||||
else {
|
||||
closeSideNav(navId);
|
||||
// Register callback for 'toggle' button
|
||||
if (toggleId) {
|
||||
|
||||
$(toggleId).click(function() {
|
||||
|
||||
var state = sessionStorage.getItem(stateLabel) || 'closed';
|
||||
var width = sessionStorage.getItem(widthLabel) || '300px';
|
||||
|
||||
if (state == 'open') {
|
||||
$(treeId).animate({
|
||||
width: '0px'
|
||||
}, 50);
|
||||
|
||||
sessionStorage.setItem(stateLabel, 'closed');
|
||||
} else {
|
||||
$(treeId).animate({
|
||||
width: width,
|
||||
}, 50);
|
||||
|
||||
sessionStorage.setItem(stateLabel, 'open');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle left-hand icon menubar display
|
||||
*/
|
||||
|
@ -43,7 +43,7 @@ from .views import DynamicJsView
|
||||
|
||||
from common.views import SettingEdit
|
||||
|
||||
from .api import InfoView
|
||||
from .api import InfoView, NotFoundView
|
||||
from .api import ActionPluginView
|
||||
|
||||
from users.urls import user_urls
|
||||
@ -70,6 +70,9 @@ apipatterns = [
|
||||
|
||||
# InvenTree information endpoint
|
||||
url(r'^$', InfoView.as_view(), name='api-inventree-info'),
|
||||
|
||||
# Unknown endpoint
|
||||
url(r'^.*$', NotFoundView.as_view(), name='api-404'),
|
||||
]
|
||||
|
||||
settings_urls = [
|
||||
|
@ -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()
|
||||
|
@ -22,7 +22,7 @@ InvenTree | {% trans "Supplier List" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class='table table-striped' id='company-table' data-toolbar='#button-toolbar'>
|
||||
<table class='table table-striped table-condensed' id='company-table' data-toolbar='#button-toolbar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -29,8 +29,6 @@ InvenTree | {% trans "Part List" %}
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
closeSideNav();
|
||||
|
||||
loadTree("{% url 'api-part-tree' %}",
|
||||
"#part-tree",
|
||||
{
|
||||
@ -38,11 +36,9 @@ InvenTree | {% trans "Part List" %}
|
||||
}
|
||||
);
|
||||
|
||||
$("#toggle-part-tree").click(function() {
|
||||
toggleSideNav("#sidenav-left");
|
||||
return false;
|
||||
initNavTree({
|
||||
label: 'part',
|
||||
treeId: '#sidenav-left',
|
||||
toggleId: '#toggle-part-tree',
|
||||
});
|
||||
|
||||
initSideNav();
|
||||
|
||||
{% endblock %}
|
@ -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):
|
||||
"""
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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' %}",
|
||||
});
|
||||
|
@ -11,7 +11,9 @@ InvenTree | Stock
|
||||
{% endblock %}
|
||||
|
||||
{% block sidenav %}
|
||||
<div id='stock-tree'></div>
|
||||
<div id='stock-tree'>
|
||||
{% trans "Loading..." %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pre_content %}
|
||||
@ -24,6 +26,7 @@ InvenTree | Stock
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadTree("{% url 'api-stock-tree' %}",
|
||||
"#stock-tree",
|
||||
{
|
||||
@ -31,10 +34,10 @@ InvenTree | Stock
|
||||
}
|
||||
);
|
||||
|
||||
$("#toggle-stock-tree").click(function() {
|
||||
toggleSideNav("#sidenav-left");
|
||||
return false;
|
||||
})
|
||||
initNavTree({
|
||||
label: 'stock',
|
||||
treeId: '#sidenav-left',
|
||||
toggleId: '#toggle-stock-tree',
|
||||
});
|
||||
|
||||
initSideNav();
|
||||
{% endblock %}
|
@ -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):
|
||||
"""
|
||||
|
@ -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,
|
||||
|
@ -630,6 +630,7 @@ function loadBuildTable(table, options) {
|
||||
url: options.url,
|
||||
queryParams: filters,
|
||||
groupBy: false,
|
||||
sidePagination: 'server',
|
||||
name: 'builds',
|
||||
original: params,
|
||||
columns: [
|
||||
|
@ -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',
|
||||
|
@ -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: [
|
||||
|
@ -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,
|
||||
|
@ -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 = "<b>" + value + "</b>";
|
||||
|
||||
@ -952,7 +956,6 @@ function loadStockTrackingTable(table, options) {
|
||||
});
|
||||
|
||||
cols.push({
|
||||
sortable: true,
|
||||
field: 'user',
|
||||
title: '{% trans "User" %}',
|
||||
formatter: function(value, row, index, field) {
|
||||
|
@ -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" %}',
|
||||
|
@ -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;
|
||||
|
||||
|
@ -5,8 +5,8 @@ from . import api
|
||||
user_urls = [
|
||||
url(r'^(?P<pk>[0-9]+)/?$', api.UserDetail.as_view(), name='user-detail'),
|
||||
|
||||
url(r'roles', api.RoleDetails.as_view(), name='api-user-roles'),
|
||||
url(r'token', api.GetAuthToken.as_view(), name='api-token'),
|
||||
url(r'roles/?$', api.RoleDetails.as_view(), name='api-user-roles'),
|
||||
url(r'token/?$', api.GetAuthToken.as_view(), name='api-token'),
|
||||
|
||||
url(r'^$', api.UserList.as_view()),
|
||||
]
|
||||
|
@ -9,6 +9,14 @@ InvenTree is designed to be lightweight and easy to use for SME or hobbyist appl
|
||||
|
||||
However, powerful business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information.
|
||||
|
||||
# Companion App
|
||||
|
||||
InvenTree is supported by a [companion mobile app](https://inventree.readthedocs.io/en/latest/app/app/) which allows users access to stock control information and functionality.
|
||||
|
||||
[**Download InvenTree from the Android Play Store**](https://play.google.com/store/apps/details?id=inventree.inventree_app)
|
||||
|
||||
*Currently the mobile app is only availble for Android*
|
||||
|
||||
# Documentation
|
||||
|
||||
For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/).
|
||||
|
Loading…
Reference in New Issue
Block a user