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 += '<td>' + item.part.name + '</td>';
if (item.location) {
html += '<td>' + item.location.name + '</td>';
} else {
html += '<td><i>No location set</i></td>';
}
html += "<td><input class='form-control' ";
html += "value='" + vCur + "' ";
html += "min='" + vMin + "' ";
@ -144,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);
});
@ -220,34 +223,28 @@ 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) {
method: 'post',
}).then(function(response) {
closeModal(modal);
if (options.success) {
options.success();
}
},
error: function(error) {
alert('error!:\n' + error);
},
method: 'post'
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 = [];
@ -262,9 +259,13 @@ function moveStockItems(items, options) {
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++) {
parts.push(items[i].pk);
@ -280,13 +281,29 @@ function moveStockItems(items, options) {
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);
$(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 '<i>No stock location set</i>';
}
}
},
@ -383,4 +400,94 @@ function loadStockTable(table, options) {
if (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:
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)
)})

View File

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

View File

@ -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,

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)
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',
]

View File

@ -121,22 +121,11 @@
{% if item.has_tracking_info %}
<hr>
<div class="panel-group">
<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 id='table-toolbar'>
<h4>Stock Tracking Information</h4>
</div>
<table class='table table-condensed table-striped' id='track-table' data-toolbar='#table-toolbar'>
</table>
{% 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') + '<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' %}",
})
});
{% 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'^track/?', views.StockTrackingIndex.as_view(), name='stock-tracking-list'),
# Individual stock items
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 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'

View File

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

View File

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