Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-04-26 21:25:00 +10:00
commit 27c7f6589c
11 changed files with 264 additions and 140 deletions

View File

@ -65,7 +65,12 @@ function updateStock(items, options={}) {
html += '<tr>'; html += '<tr>';
html += '<td>' + item.part.name + '</td>'; html += '<td>' + item.part.name + '</td>';
if (item.location) {
html += '<td>' + item.location.name + '</td>'; html += '<td>' + item.location.name + '</td>';
} else {
html += '<td><i>No location set</i></td>';
}
html += "<td><input class='form-control' "; html += "<td><input class='form-control' ";
html += "value='" + vCur + "' "; html += "value='" + vCur + "' ";
html += "min='" + vMin + "' "; html += "min='" + vMin + "' ";
@ -144,9 +149,7 @@ function updateStock(items, options={}) {
method: 'post', method: 'post',
}).then(function(response) { }).then(function(response) {
closeModal(modal); closeModal(modal);
if (options.success) { afterForm(response, options);
options.success();
}
}).fail(function(xhr, status, error) { }).fail(function(xhr, status, error) {
alert(error); alert(error);
}); });
@ -220,34 +223,28 @@ function moveStockItems(items, options) {
return; return;
} }
function doMove(location, parts) { function doMove(location, parts, notes) {
inventreeUpdate("/api/stock/move/", inventreeUpdate("/api/stock/move/",
{ {
location: location, location: location,
'parts[]': parts 'parts[]': parts,
'notes': notes,
}, },
{ {
success: function(response) { method: 'post',
}).then(function(response) {
closeModal(modal); closeModal(modal);
if (options.success) { afterForm(response, options);
options.success(); }).fail(function(xhr, status, error) {
} alert(error);
},
error: function(error) {
alert('error!:\n' + error);
},
method: 'post'
}); });
} }
getStockLocations({}, getStockLocations({},
{ {
success: function(response) { success: function(response) {
openModal({
modal: modal,
title: "Move " + items.length + " stock items",
submit_text: "Move"
});
// Extact part row info // Extact part row info
var parts = []; var parts = [];
@ -262,9 +259,13 @@ function moveStockItems(items, options) {
html += makeOption(loc.pk, loc.name + ' - <i>' + loc.description + '</i>'); html += makeOption(loc.pk, loc.name + ' - <i>' + loc.description + '</i>');
} }
html += "</select><br><hr>"; html += "</select><br>";
html += "The following stock items will be moved:<br><ul class='list-group'>\n"; html += "<hr><input type='text' id='notes' placeholder='Notes'/>";
html += "<p class='warning-msg' id='note-warning'><i>Note field must be filled</i></p>";
html += "<hr>The following stock items will be moved:<br><ul class='list-group'>\n";
for (i = 0; i < items.length; i++) { for (i = 0; i < items.length; i++) {
parts.push(items[i].pk); parts.push(items[i].pk);
@ -280,13 +281,29 @@ function moveStockItems(items, options) {
html += "</ul>\n"; html += "</ul>\n";
modalSetContent(modal, html); openModal({
modal: modal,
title: "Move " + items.length + " stock items",
submit_text: "Move",
content: html
});
//modalSetContent(modal, html);
attachSelect(modal); attachSelect(modal);
$(modal).find('#note-warning').hide();
modalSubmit(modal, function() { modalSubmit(modal, function() {
var locId = $(modal).find("#stock-location").val(); 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) { error: function(error) {
@ -358,7 +375,7 @@ function loadStockTable(table, options) {
return renderLink(row.location.pathstring, row.location.url); return renderLink(row.location.pathstring, row.location.url);
} }
else { else {
return ''; return '<i>No stock location set</i>';
} }
} }
}, },
@ -383,4 +400,94 @@ function loadStockTable(table, options) {
if (options.buttons) { if (options.buttons) {
linkButtonsToSelection(table, options.buttons); linkButtonsToSelection(table, options.buttons);
} }
}; }
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') + '<br>' + 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 = "<b>" + value + "</b>";
if (row.notes) {
html += "<br><i>" + row.notes + "</i>";
}
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);
}
}

View File

@ -111,17 +111,22 @@ class StockStocktake(APIView):
if 'notes' in request.data: if 'notes' in request.data:
notes = request.data['notes'] notes = request.data['notes']
n = 0
for item in items: for item in items:
quantity = int(item['quantity']) quantity = int(item['quantity'])
if action == u'stocktake': 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': 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': 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): class StockMove(APIView):
@ -153,6 +158,9 @@ class StockMove(APIView):
errors = [] errors = []
if u'notes' not in data:
errors.append({'notes': 'Notes field must be supplied'})
for pid in part_list: for pid in part_list:
try: try:
part = StockItem.objects.get(pk=pid) part = StockItem.objects.get(pk=pid)
@ -163,12 +171,15 @@ class StockMove(APIView):
if len(errors) > 0: if len(errors) > 0:
raise ValidationError(errors) raise ValidationError(errors)
n = 0
for part in parts: 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( return Response({'success': 'Moved {n} parts to {loc}'.format(
n=len(parts), n=n,
loc=location.name loc=str(location)
)}) )})

View File

@ -64,7 +64,9 @@ class EditStockItemForm(HelperForm):
model = StockItem model = StockItem
fields = [ fields = [
'supplier_part',
'batch', 'batch',
'status', 'status',
'notes' 'notes',
'URL',
] ]

View File

@ -67,6 +67,7 @@ class StockItem(models.Model):
self.add_transaction_note( self.add_transaction_note(
'Created stock item', 'Created stock item',
None, None,
notes="Created new stock item for part '{p}'".format(p=str(self.part)),
system=True system=True
) )
@ -220,13 +221,17 @@ class StockItem(models.Model):
@transaction.atomic @transaction.atomic
def move(self, location, notes, user): def move(self, location, notes, user):
if location.pk == self.location.pk: if location is None:
return False # raise forms.ValidationError("Cannot move item to its current location") # 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( msg = "Moved to {loc}".format(loc=str(location))
loc=location.name,
src=self.location.name if self.location:
) msg += " (from {loc})".format(loc=str(self.location))
self.location = location self.location = location
self.save() self.save()
@ -329,7 +334,8 @@ class StockItemTracking(models.Model):
""" """
def get_absolute_url(self): 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 # Stock item
item = models.ForeignKey(StockItem, on_delete=models.CASCADE, item = models.ForeignKey(StockItem, on_delete=models.CASCADE,

View File

@ -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) 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: class Meta:
model = StockItemTracking model = StockItem
fields = [ fields = [
'pk', 'pk',
'uuid',
'url', 'url',
'item', 'part_name',
'date',
'title',
'notes',
'quantity',
'user',
'system',
]
read_only_fields = [
'date',
'user',
'system',
'quantity',
] ]
@ -118,3 +109,33 @@ class LocationSerializer(serializers.ModelSerializer):
'parent', 'parent',
'pathstring' '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',
]

View File

@ -121,22 +121,11 @@
{% if item.has_tracking_info %} {% if item.has_tracking_info %}
<hr> <div id='table-toolbar'>
<div class="panel-group"> <h4>Stock Tracking Information</h4>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#collapse1">Stock Tracking</a><span class='badge'>{{ item.tracking_info.all|length }}</span>
</h4>
</div>
<div id="collapse1" class="panel-collapse collapse">
<div class="panel-body">
<table class='table table-condensed table-striped' id='track-table'>
</table>
</div>
</div>
</div>
</div> </div>
<table class='table table-condensed table-striped' id='track-table' data-toolbar='#table-toolbar'>
</table>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block js_ready %} {% block js_ready %}
@ -210,66 +199,14 @@
}); });
}); });
$('#track-table').bootstrapTable({ loadStockTrackingTable($("#track-table"), {
sortable: true, params: function(p) {
search: true,
method: 'get',
queryParams: function(p) {
return { return {
ordering: '-date',
item: {{ item.pk }}, 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') + '<br>' + m.format('h:mm a');
return html;
}
return 'N/A';
}
},
{
field: 'title',
title: 'Description',
sortable: true,
formatter: function(value, row, index, field) {
var html = "<b>" + value + "</b>";
if (row.notes) {
html += "<br><i>" + row.notes + "</i>";
}
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 %} {% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "stock/stock_app_base.html" %}
{% load static %}
{% block content %}
<h3>Stock list here!</h3>
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='tracking-table'>
</table>
{% 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 %}

View File

@ -28,6 +28,8 @@ stock_urls = [
url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'), 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 # Individual stock items
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)), url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),

View File

@ -9,7 +9,7 @@ from django.forms.models import model_to_dict
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from part.models import Part from part.models import Part
from .models import StockItem, StockLocation from .models import StockItem, StockLocation, StockItemTracking
from .forms import EditStockLocationForm from .forms import EditStockLocationForm
from .forms import CreateStockItemForm from .forms import CreateStockItemForm
@ -248,3 +248,13 @@ class StockItemStocktake(AjaxUpdateView):
} }
return self.renderJsonResponse(request, form, data) 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'

View File

@ -8,7 +8,7 @@ clean:
rm -f .coverage rm -f .coverage
style: style:
flake8 InvenTree --ignore=C901,E501 flake8 InvenTree
test: test:
python InvenTree/manage.py check python InvenTree/manage.py check

View File

@ -5,4 +5,4 @@ ignore =
# - E501 - line too long (82 characters) # - E501 - line too long (82 characters)
E501 E501
exclude = .git,__pycache__ exclude = .git,__pycache__
max-complexity = 10 max-complexity = 20