From 533fdb71c45aec63f81e06b202fa47ab42c42ad2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 22:04:02 +1000 Subject: [PATCH 1/9] Javascript function to render stock tracking table - Added extra info to StockItemTracking serializer --- InvenTree/static/script/inventree/stock.js | 91 +++++++++++++++++++++- InvenTree/stock/serializers.py | 55 +++++++++---- InvenTree/stock/templates/stock/item.html | 83 +++----------------- 3 files changed, 138 insertions(+), 91 deletions(-) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 9785cc3c3d..91aec8c6d1 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -383,4 +383,93 @@ function loadStockTable(table, options) { if (options.buttons) { linkButtonsToSelection(table, options.buttons); } -}; \ No newline at end of file +} + + +function loadStockTrackingTable(table, options) { + + var cols = [ + { + field: 'pk', + visible: false, + }, + { + field: 'date', + title: 'Date', + sortable: true, + formatter: function(value, row, index, field) { + var m = moment(value); + if (m.isValid()) { + var html = m.format('dddd MMMM Do YYYY') + '
' + m.format('h:mm a'); + return html; + } + + return 'N/A'; + } + }, + ]; + + // If enabled, provide a link to the referenced StockItem + if (options.partColumn) { + cols.push({ + field: 'item', + title: 'Stock Item', + sortable: true, + formatter: function(value, row, index, field) { + return renderLink(value.part_name, value.url); + } + }); + } + + // Stock transaction description + cols.push({ + field: 'title', + title: 'Description', + sortable: true, + formatter: function(value, row, index, field) { + var html = "" + value + ""; + + if (row.notes) { + html += "
" + row.notes + ""; + } + + return html; + } + }); + + cols.push({ + field: 'quantity', + title: 'Quantity', + }); + + cols.push({ + sortable: true, + field: 'user', + title: 'User', + formatter: function(value, row, index, field) { + if (value) + { + // TODO - Format the user's first and last names + return value.username; + } + else + { + return "No user information"; + } + } + }); + + table.bootstrapTable({ + sortable: true, + search: true, + method: 'get', + rememberOrder: true, + queryParams: options.params, + columns: cols, + url: options.url, + }); + + if (options.buttons) { + linkButtonsToSelection(table, options.buttons); + } +} \ No newline at end of file diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index f79ceaa3b1..64baeb5330 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -24,31 +24,22 @@ class LocationBriefSerializer(serializers.ModelSerializer): ] -class StockTrackingSerializer(serializers.ModelSerializer): +class StockItemSerializerBrief(serializers.ModelSerializer): + """ + Provide a brief serializer for StockItem + """ url = serializers.CharField(source='get_absolute_url', read_only=True) - user = UserSerializerBrief(many=False, read_only=True) + part_name = serializers.CharField(source='part.name', read_only=True) class Meta: - model = StockItemTracking + model = StockItem fields = [ 'pk', + 'uuid', 'url', - 'item', - 'date', - 'title', - 'notes', - 'quantity', - 'user', - 'system', - ] - - read_only_fields = [ - 'date', - 'user', - 'system', - 'quantity', + 'part_name', ] @@ -118,3 +109,33 @@ class LocationSerializer(serializers.ModelSerializer): 'parent', 'pathstring' ] + + +class StockTrackingSerializer(serializers.ModelSerializer): + + url = serializers.CharField(source='get_absolute_url', read_only=True) + + user = UserSerializerBrief(many=False, read_only=True) + + item = StockItemSerializerBrief(many=False, read_only=True) + + class Meta: + model = StockItemTracking + fields = [ + 'pk', + 'url', + 'item', + 'date', + 'title', + 'notes', + 'quantity', + 'user', + 'system', + ] + + read_only_fields = [ + 'date', + 'user', + 'system', + 'quantity', + ] diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index aee40d513d..cd2de3bf0d 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -121,22 +121,11 @@ {% if item.has_tracking_info %} -
-
-
-
-

- Stock Tracking{{ item.tracking_info.all|length }} -

-
-
-
- -
-
-
-
+
+

Stock Tracking Information

+ +
{% endif %} {% endblock %} {% block js_ready %} @@ -210,66 +199,14 @@ }); }); - $('#track-table').bootstrapTable({ - sortable: true, - search: true, - method: 'get', - queryParams: function(p) { + loadStockTrackingTable($("#track-table"), { + params: function(p) { return { + ordering: '-date', item: {{ item.pk }}, - } + }; }, - columns: [ - { - field: 'date', - title: 'Date', - sortable: true, - formatter: function(value, row, index, field) { - var m = moment(value); - if (m.isValid()) { - var html = m.format('dddd MMMM Do YYYY') + '
' + m.format('h:mm a'); - return html; - } - - return 'N/A'; - } - }, - { - field: 'title', - title: 'Description', - sortable: true, - formatter: function(value, row, index, field) { - var html = "" + value + ""; - - if (row.notes) { - html += "
" + row.notes + ""; - } - - return html; - } - }, - { - field: 'quantity', - title: 'Quantity', - }, - { - sortable: true, - field: 'user', - title: 'User', - formatter: function(value, row, index, field) { - if (value) - { - // TODO - Format the user's first and last names - return value.username; - } - else - { - return "No user information"; - } - } - } - ], - url: "{% url 'api-stock-track' %}", - }) + url: "{% url 'api-stock-track' %}", + }); {% endblock %} \ No newline at end of file From 79f8736b6b439fdead3dc8754096f75e31f3dccf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 22:10:46 +1000 Subject: [PATCH 2/9] Add note on creation of StockItem --- InvenTree/stock/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 7333cc4f7f..03aaac7243 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -67,6 +67,7 @@ class StockItem(models.Model): self.add_transaction_note( 'Created stock item', None, + notes="Created new stock item for part '{p}'".format(p=str(self.part)), system=True ) From 053e9c9795bf58e77a42ab619b229065b6fd9620 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 22:11:10 +1000 Subject: [PATCH 3/9] Add a stock tracking index - Shows entire history of stocktracking items --- InvenTree/stock/models.py | 3 +- InvenTree/stock/templates/stock/tracking.html | 28 +++++++++++++++++++ InvenTree/stock/urls.py | 2 ++ InvenTree/stock/views.py | 12 +++++++- 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 InvenTree/stock/templates/stock/tracking.html diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 03aaac7243..931a62d689 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -330,7 +330,8 @@ class StockItemTracking(models.Model): """ def get_absolute_url(self): - return reverse('stock-tracking-detail', kwargs={'pk': self.id}) + return '/stock/track/{pk}'.format(pk=self.id) + #return reverse('stock-tracking-detail', kwargs={'pk': self.id}) # Stock item item = models.ForeignKey(StockItem, on_delete=models.CASCADE, diff --git a/InvenTree/stock/templates/stock/tracking.html b/InvenTree/stock/templates/stock/tracking.html new file mode 100644 index 0000000000..9ba4b290df --- /dev/null +++ b/InvenTree/stock/templates/stock/tracking.html @@ -0,0 +1,28 @@ +{% extends "stock/stock_app_base.html" %} +{% load static %} + +{% block content %} + +

Stock list here!

+ + +
+ +{% include 'modals.html' %} + +{% endblock %} + +{% block js_ready %} +{{ block.super }} + + loadStockTrackingTable($("#tracking-table"), { + params: function(p) { + return { + ordering: '-date', + }; + }, + partColumn: true, + url: "{% url 'api-stock-track' %}", + }); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 090ffcac53..503c9e809c 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -28,6 +28,8 @@ stock_urls = [ url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'), + url(r'^track/?', views.StockTrackingIndex.as_view(), name='stock-tracking-list'), + # Individual stock items url(r'^item/(?P\d+)/', include(stock_item_detail_urls)), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 207b2e698a..fc1d2726aa 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -9,7 +9,7 @@ from django.forms.models import model_to_dict from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from part.models import Part -from .models import StockItem, StockLocation +from .models import StockItem, StockLocation, StockItemTracking from .forms import EditStockLocationForm from .forms import CreateStockItemForm @@ -248,3 +248,13 @@ class StockItemStocktake(AjaxUpdateView): } return self.renderJsonResponse(request, form, data) + + +class StockTrackingIndex(ListView): + """ + StockTrackingIndex provides a page to display StockItemTracking objects + """ + + model = StockItemTracking + template_name = 'stock/tracking.html' + context_object_name = 'items' From 38fa89d1da7e7f483f582dc58409be430a80ff60 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 22:34:30 +1000 Subject: [PATCH 4/9] Bug fix for javascript - If location not set for StockItem, display special text --- InvenTree/static/script/inventree/stock.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 91aec8c6d1..a858dca122 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -65,7 +65,12 @@ function updateStock(items, options={}) { html += ''; html += '' + item.part.name + ''; - html += '' + item.location.name + ''; + + if (item.location) { + html += '' + item.location.name + ''; + } else { + html += 'No location set'; + } html += " Date: Thu, 25 Apr 2019 23:01:03 +1000 Subject: [PATCH 5/9] Enforce 'notes' field for StockItem move - Better error handling for StockItem.move --- InvenTree/static/script/inventree/stock.js | 26 +++++++++++++++++----- InvenTree/stock/api.py | 5 ++++- InvenTree/stock/models.py | 16 ++++++++----- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index a858dca122..a32e446ad6 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -225,11 +225,12 @@ function moveStockItems(items, options) { return; } - function doMove(location, parts) { + function doMove(location, parts, notes) { inventreeUpdate("/api/stock/move/", { location: location, - 'parts[]': parts + 'parts[]': parts, + 'notes': notes, }, { success: function(response) { @@ -267,9 +268,13 @@ function moveStockItems(items, options) { html += makeOption(loc.pk, loc.name + ' - ' + loc.description + ''); } - html += "

"; + html += "
"; - html += "The following stock items will be moved:
    \n"; + html += "
    "; + + html += "

    Note field must be filled

    "; + + html += "
    The following stock items will be moved:
      \n"; for (i = 0; i < items.length; i++) { parts.push(items[i].pk); @@ -288,10 +293,19 @@ function moveStockItems(items, options) { modalSetContent(modal, html); attachSelect(modal); + $(modal).find('#note-warning').hide(); + modalSubmit(modal, function() { var locId = $(modal).find("#stock-location").val(); - doMove(locId, parts); + var notes = $(modal).find('#notes').val(); + + if (!notes) { + $(modal).find('#note-warning').show(); + return false; + } + + doMove(locId, parts, notes); }); }, error: function(error) { @@ -363,7 +377,7 @@ function loadStockTable(table, options) { return renderLink(row.location.pathstring, row.location.url); } else { - return ''; + return 'No stock location set'; } } }, diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 32e3807ef7..2aae16e58b 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -153,6 +153,9 @@ class StockMove(APIView): errors = [] + if u'notes' not in data: + errors.append({'notes': 'Notes field must be supplied'}) + for pid in part_list: try: part = StockItem.objects.get(pk=pid) @@ -164,7 +167,7 @@ class StockMove(APIView): raise ValidationError(errors) for part in parts: - part.move(location, request.user) + part.move(location, data.get('notes'), request.user) return Response({'success': 'Moved {n} parts to {loc}'.format( n=len(parts), diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 931a62d689..5dc087dcd5 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -221,13 +221,17 @@ class StockItem(models.Model): @transaction.atomic def move(self, location, notes, user): - if location.pk == self.location.pk: - return False # raise forms.ValidationError("Cannot move item to its current location") + if location is None: + # TODO - Raise appropriate error (cannot move to blank location) + return False + elif self.location and (location.pk == self.location.pk): + # TODO - Raise appropriate error (cannot move to same location) + return False - msg = "Moved to {loc} (from {src})".format( - loc=location.name, - src=self.location.name - ) + msg = "Moved to {loc}".format(loc=str(location)) + + if self.location: + msg += " (from {loc})".format(loc=str(self.location)) self.location = location self.save() From 990808ec03b1dd4f8be9b6095501aa2fcc225aab Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 23:16:54 +1000 Subject: [PATCH 6/9] Fix code to move multiple parts via AJAX / JSON --- InvenTree/static/script/inventree/stock.js | 50 +++++++++++----------- InvenTree/stock/api.py | 9 ++-- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index a32e446ad6..e117ef0240 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -149,9 +149,7 @@ function updateStock(items, options={}) { method: 'post', }).then(function(response) { closeModal(modal); - if (options.success) { - options.success(); - } + afterForm(response, options); }).fail(function(xhr, status, error) { alert(error); }); @@ -227,33 +225,26 @@ function moveStockItems(items, options) { function doMove(location, parts, notes) { inventreeUpdate("/api/stock/move/", - { - location: location, - 'parts[]': parts, - 'notes': notes, - }, - { - success: function(response) { - closeModal(modal); - if (options.success) { - options.success(); - } - }, - error: function(error) { - alert('error!:\n' + error); - }, - method: 'post' - }); + { + location: location, + 'parts[]': parts, + 'notes': notes, + }, + { + method: 'post', + }).then(function(response) { + closeModal(modal); + afterForm(response, options); + }).fail(function(xhr, status, error) { + alert(error); + }); } + getStockLocations({}, { success: function(response) { - openModal({ - modal: modal, - title: "Move " + items.length + " stock items", - submit_text: "Move" - }); + // Extact part row info var parts = []; @@ -290,7 +281,14 @@ function moveStockItems(items, options) { html += "
    \n"; - modalSetContent(modal, html); + openModal({ + modal: modal, + title: "Move " + items.length + " stock items", + submit_text: "Move", + content: html + }); + + //modalSetContent(modal, html); attachSelect(modal); $(modal).find('#note-warning').hide(); diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 2aae16e58b..2ac2ea584e 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -166,12 +166,15 @@ class StockMove(APIView): if len(errors) > 0: raise ValidationError(errors) + n = 0 + for part in parts: - part.move(location, data.get('notes'), request.user) + if part.move(location, data.get('notes'), request.user): + n += 1 return Response({'success': 'Moved {n} parts to {loc}'.format( - n=len(parts), - loc=location.name + n=n, + loc=str(location) )}) From 1f8632c77cc7186c458ed1d1259d993c13b49145 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 23:19:22 +1000 Subject: [PATCH 7/9] Improved response message for stocktake --- InvenTree/stock/api.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 2ac2ea584e..ca1d0b9988 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -111,17 +111,22 @@ class StockStocktake(APIView): if 'notes' in request.data: notes = request.data['notes'] + n = 0 + for item in items: quantity = int(item['quantity']) if action == u'stocktake': - item['item'].stocktake(quantity, request.user, notes=notes) + if item['item'].stocktake(quantity, request.user, notes=notes): + n += 1 elif action == u'remove': - item['item'].take_stock(quantity, request.user, notes=notes) + if item['item'].take_stock(quantity, request.user, notes=notes): + n += 1 elif action == u'add': - item['item'].add_stock(quantity, request.user, notes=notes) + if item['item'].add_stock(quantity, request.user, notes=notes): + n += 1 - return Response({'success': 'success'}) + return Response({'success': 'Updated stock for {n} items'.format(n=n)}) class StockMove(APIView): From 8d5850248ea2df3025b612f7024bb24b4bd737ce Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 23:25:52 +1000 Subject: [PATCH 8/9] PEP fixes --- InvenTree/stock/api.py | 2 +- InvenTree/stock/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index ca1d0b9988..52edd2945e 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -171,7 +171,7 @@ class StockMove(APIView): if len(errors) > 0: raise ValidationError(errors) - n = 0 + n = 0 for part in parts: if part.move(location, data.get('notes'), request.user): diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 5dc087dcd5..4b1505d2d0 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -335,7 +335,7 @@ class StockItemTracking(models.Model): def get_absolute_url(self): return '/stock/track/{pk}'.format(pk=self.id) - #return reverse('stock-tracking-detail', kwargs={'pk': self.id}) + # return reverse('stock-tracking-detail', kwargs={'pk': self.id}) # Stock item item = models.ForeignKey(StockItem, on_delete=models.CASCADE, From d78841a665f8582f80f12641aacd2b85d4f6f8ca Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Apr 2019 23:35:48 +1000 Subject: [PATCH 9/9] Allow editing of more stockitem options --- InvenTree/static/script/inventree/stock.js | 1 + InvenTree/stock/forms.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index e117ef0240..ac7d151413 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -483,6 +483,7 @@ function loadStockTrackingTable(table, options) { rememberOrder: true, queryParams: options.params, columns: cols, + pagination: true, url: options.url, }); diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index f6f9032aab..19dd5b2986 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -64,7 +64,9 @@ class EditStockItemForm(HelperForm): model = StockItem fields = [ + 'supplier_part', 'batch', 'status', - 'notes' + 'notes', + 'URL', ]