mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
27c7f6589c
@ -65,7 +65,12 @@ function updateStock(items, options={}) {
|
||||
html += '<tr>';
|
||||
|
||||
html += '<td>' + item.part.name + '</td>';
|
||||
html += '<td>' + item.location.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
|
||||
},
|
||||
{
|
||||
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 = [];
|
||||
@ -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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
)})
|
||||
|
||||
|
||||
|
@ -64,7 +64,9 @@ class EditStockItemForm(HelperForm):
|
||||
model = StockItem
|
||||
|
||||
fields = [
|
||||
'supplier_part',
|
||||
'batch',
|
||||
'status',
|
||||
'notes'
|
||||
'notes',
|
||||
'URL',
|
||||
]
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
]
|
||||
|
@ -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' %}",
|
||||
})
|
||||
url: "{% url 'api-stock-track' %}",
|
||||
});
|
||||
|
||||
{% endblock %}
|
28
InvenTree/stock/templates/stock/tracking.html
Normal file
28
InvenTree/stock/templates/stock/tracking.html
Normal 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 %}
|
@ -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)),
|
||||
|
||||
|
@ -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'
|
||||
|
2
Makefile
2
Makefile
@ -8,7 +8,7 @@ clean:
|
||||
rm -f .coverage
|
||||
|
||||
style:
|
||||
flake8 InvenTree --ignore=C901,E501
|
||||
flake8 InvenTree
|
||||
|
||||
test:
|
||||
python InvenTree/manage.py check
|
||||
|
Loading…
Reference in New Issue
Block a user