diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js
index 9785cc3c3d..ac7d151413 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 += "' + 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);
@@ -280,13 +281,29 @@ 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();
+
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) {
@@ -358,7 +375,7 @@ function loadStockTable(table, options) {
return renderLink(row.location.pathstring, row.location.url);
}
else {
- return '';
+ return 'No stock location set';
}
}
},
@@ -383,4 +400,94 @@ 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,
+ pagination: true,
+ url: options.url,
+ });
+
+ if (options.buttons) {
+ linkButtonsToSelection(table, options.buttons);
+ }
+}
\ No newline at end of file
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index 32e3807ef7..52edd2945e 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):
@@ -153,6 +158,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)
@@ -163,12 +171,15 @@ class StockMove(APIView):
if len(errors) > 0:
raise ValidationError(errors)
+ n = 0
+
for part in parts:
- part.move(location, 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)
)})
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',
]
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index 7333cc4f7f..4b1505d2d0 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
)
@@ -220,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()
@@ -329,7 +334,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/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 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
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'
diff --git a/Makefile b/Makefile
index 7a1a97bb46..686b188b3c 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@ clean:
rm -f .coverage
style:
- flake8 InvenTree --ignore=C901,E501
+ flake8 InvenTree
test:
python InvenTree/manage.py check
diff --git a/setup.cfg b/setup.cfg
index ea418b2daf..f6f50b0c03 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,4 +5,4 @@ ignore =
# - E501 - line too long (82 characters)
E501
exclude = .git,__pycache__
-max-complexity = 10
+max-complexity = 20
|