diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 52f38bea18..06c06bde05 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -125,6 +125,13 @@ class InvenTreeSetting(models.Model): 'validator': bool }, + 'PART_RECENT_COUNT': { + 'name': _('Recent Part Count'), + 'description': _('Number of recent parts to display on index page'), + 'default': 10, + 'validator': [int, MinValueValidator(1)] + }, + 'PART_TEMPLATE': { 'name': _('Template'), 'description': _('Parts are templates by default'), @@ -249,6 +256,13 @@ class InvenTreeSetting(models.Model): 'validator': bool, }, + 'STOCK_RECENT_COUNT': { + 'name': _('Recent Stock Count'), + 'description': _('Number of recent stock items to display on index page'), + 'default': 10, + 'validator': [int, MinValueValidator(1)] + }, + 'BUILDORDER_REFERENCE_PREFIX': { 'name': _('Build Order Reference Prefix'), 'description': _('Prefix value for build order reference'), @@ -521,12 +535,18 @@ class InvenTreeSetting(models.Model): validator = InvenTreeSetting.get_setting_validator(self.key) - if validator is not None: - self.run_validator(validator) - if self.is_bool(): self.value = InvenTree.helpers.str2bool(self.value) + if self.is_int(): + try: + self.value = int(self.value) + except (ValueError): + raise ValidationError(_('Must be an integer value')) + + if validator is not None: + self.run_validator(validator) + def run_validator(self, validator): """ Run a validator against the 'value' field for this InvenTreeSetting object. @@ -535,39 +555,39 @@ class InvenTreeSetting(models.Model): if validator is None: return - # If a list of validators is supplied, iterate through each one - if type(validator) in [list, tuple]: - for v in validator: - self.run_validator(v) - - return - - if callable(validator): - # We can accept function validators with a single argument - print("Running validator function") - validator(self.value) + value = self.value # Boolean validator - if validator == bool: + if self.is_bool(): # Value must "look like" a boolean value - if InvenTree.helpers.is_bool(self.value): + if InvenTree.helpers.is_bool(value): # Coerce into either "True" or "False" - self.value = str(InvenTree.helpers.str2bool(self.value)) + value = InvenTree.helpers.str2bool(value) else: raise ValidationError({ 'value': _('Value must be a boolean value') }) # Integer validator - if validator == int: + if self.is_int(): + try: # Coerce into an integer value - self.value = str(int(self.value)) + value = int(value) except (ValueError, TypeError): raise ValidationError({ 'value': _('Value must be an integer value'), }) + # If a list of validators is supplied, iterate through each one + if type(validator) in [list, tuple]: + for v in validator: + self.run_validator(v) + + if callable(validator): + # We can accept function validators with a single argument + validator(self.value) + def validate_unique(self, exclude=None): """ Ensure that the key:value pair is unique. In addition to the base validators, this ensures that the 'key' @@ -597,7 +617,13 @@ class InvenTreeSetting(models.Model): validator = InvenTreeSetting.get_setting_validator(self.key) - return validator == bool + if validator == bool: + return True + + if type(validator) in [list, tuple]: + for v in validator: + if v == bool: + return True def as_bool(self): """ @@ -623,6 +649,8 @@ class InvenTreeSetting(models.Model): if v == int: return True + return False + def as_int(self): """ Return the value of this setting converted to a boolean value. diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 04a37e5fff..12bfd2de1d 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -626,7 +626,7 @@ class PartList(generics.ListCreateAPIView): queryset = queryset.filter(pk__in=parts_need_stock) - # Limit choices + # Limit number of results limit = params.get('limit', None) if limit is not None: diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 4e532b90b7..75b0d9de3b 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -808,6 +808,19 @@ class StockList(generics.ListCreateAPIView): print("After error:", str(updated_after)) pass + # Limit number of results + limit = params.get('limit', None) + + if limit is not None: + try: + limit = int(limit) + + if limit > 0: + queryset = queryset[:limit] + + except (ValueError): + pass + # Also ensure that we pre-fecth all the related items queryset = queryset.prefetch_related( 'part', @@ -815,8 +828,6 @@ class StockList(generics.ListCreateAPIView): 'location' ) - queryset = queryset.order_by('part__name') - return queryset filter_backends = [ @@ -828,6 +839,15 @@ class StockList(generics.ListCreateAPIView): filter_fields = [ ] + ordering_fields = [ + 'part__name', + 'updated', + 'stocktake_date', + 'expiry_date', + ] + + ordering = ['part__name'] + search_fields = [ 'serial', 'batch', diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index 687ceaf0ee..cb2cda0a30 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: 10, + limit: {% settings_value "PART_RECENT_COUNT" %}, }, name: 'latest_parts', }); @@ -125,8 +125,19 @@ loadSimplePartTable("#table-bom-validation", "{% url 'api-part-list' %}", { {% if roles.stock.view %} addHeaderTitle('{% trans "Stock" %}'); +addHeaderAction('recently-updated-stock', '{% trans "Recently Updated" %}', 'fa-clock'); addHeaderAction('low-stock', '{% trans "Low Stock" %}', 'fa-shopping-cart'); addHeaderAction('stock-to-build', '{% trans "Required for Build Orders" %}', 'fa-bullhorn'); + +loadStockTable($('#table-recently-updated-stock'), { + params: { + ordering: "-updated", + limit: {% settings_value "STOCK_RECENT_COUNT" %}, + }, + name: 'recently-updated-stock', + grouping: false, +}); + {% settings_value "STOCK_ENABLE_EXPIRY" as expiry %} {% if expiry %} addHeaderAction('expired-stock', '{% trans "Expired Stock" %}', 'fa-calendar-times'); diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index 9174b2f127..bef951e203 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -19,6 +19,7 @@ {% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %} {% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" %} + {% include "InvenTree/settings/setting.html" with key="PART_RECENT_COUNT" icon="fa-clock" %} {% include "InvenTree/settings/setting.html" with key="PART_TEMPLATE" icon="fa-clone" %} {% include "InvenTree/settings/setting.html" with key="PART_ASSEMBLY" icon="fa-tools" %} diff --git a/InvenTree/templates/InvenTree/settings/stock.html b/InvenTree/templates/InvenTree/settings/stock.html index 9c82202a02..7909e11a60 100644 --- a/InvenTree/templates/InvenTree/settings/stock.html +++ b/InvenTree/templates/InvenTree/settings/stock.html @@ -16,6 +16,7 @@ {% include "InvenTree/settings/header.html" %} {% include "InvenTree/settings/setting.html" with key="STOCK_GROUP_BY_PART" icon="fa-layer-group" %} + {% include "InvenTree/settings/setting.html" with key="STOCK_RECENT_COUNT" icon="fa-clock" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ENABLE_EXPIRY" icon="fa-stopwatch" %} {% include "InvenTree/settings/setting.html" with key="STOCK_STALE_DAYS" icon="fa-calendar" %} {% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_SALE" icon="fa-truck" %} diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index f1794f3664..b115b8171c 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -319,6 +319,12 @@ function loadStockTable(table, options) { } } + var grouping = true; + + if ('grouping' in options) { + grouping = options.grouping; + } + table.inventreeTable({ method: 'get', formatNoMatches: function() { @@ -333,7 +339,7 @@ function loadStockTable(table, options) { {% settings_value 'STOCK_GROUP_BY_PART' as group_by_part %} {% if group_by_part %} groupByField: options.groupByField || 'part', - groupBy: true, + groupBy: grouping, groupByFormatter: function(field, id, data) { var row = data[0];