mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-01 11:10:54 +00:00
Merge remote-tracking branch 'inventree/master'
# Conflicts: # InvenTree/static/script/inventree/stock.js # InvenTree/stock/forms.py # InvenTree/stock/urls.py # InvenTree/stock/views.py
This commit is contained in:
@ -470,9 +470,9 @@ stock_api_urls = [
|
||||
|
||||
url(r'location/(?P<pk>\d+)/', include(location_endpoints)),
|
||||
|
||||
url(r'stocktake/?', StockStocktake.as_view(), name='api-stock-stocktake'),
|
||||
|
||||
url(r'move/?', StockMove.as_view(), name='api-stock-move'),
|
||||
# These JSON endpoints have been replaced (for now) with server-side form rendering - 02/06/2019
|
||||
# url(r'stocktake/?', StockStocktake.as_view(), name='api-stock-stocktake'),
|
||||
# url(r'move/?', StockMove.as_view(), name='api-stock-move'),
|
||||
|
||||
url(r'track/?', StockTrackingList.as_view(), name='api-stock-track'),
|
||||
|
||||
|
@ -42,8 +42,16 @@ class CreateStockItemForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class MoveStockItemForm(forms.ModelForm):
|
||||
""" Form for moving a StockItem to a new location """
|
||||
class AdjustStockForm(forms.ModelForm):
|
||||
""" Form for performing simple stock adjustments.
|
||||
|
||||
- Add stock
|
||||
- Remove stock
|
||||
- Count stock
|
||||
- Move stock
|
||||
|
||||
This form is used for managing stock adjuments for single or multiple stock items.
|
||||
"""
|
||||
|
||||
def get_location_choices(self):
|
||||
locs = StockLocation.objects.all()
|
||||
@ -55,37 +63,27 @@ class MoveStockItemForm(forms.ModelForm):
|
||||
|
||||
return choices
|
||||
|
||||
location = forms.ChoiceField(label='Destination', required=True, help_text='Destination stock location')
|
||||
destination = forms.ChoiceField(label='Destination', required=True, help_text='Destination stock location')
|
||||
note = forms.CharField(label='Notes', required=True, help_text='Add note (required)')
|
||||
transaction = forms.BooleanField(required=False, initial=False, label='Create Transaction', help_text='Create a stock transaction for these parts')
|
||||
# transaction = forms.BooleanField(required=False, initial=False, label='Create Transaction', help_text='Create a stock transaction for these parts')
|
||||
confirm = forms.BooleanField(required=False, initial=False, label='Confirm Stock Movement', help_text='Confirm movement of stock items')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MoveStockItemForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields['location'].choices = self.get_location_choices()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['destination'].choices = self.get_location_choices()
|
||||
|
||||
class Meta:
|
||||
model = StockItem
|
||||
|
||||
fields = [
|
||||
'location',
|
||||
'destination',
|
||||
'note',
|
||||
'transaction',
|
||||
# 'transaction',
|
||||
'confirm',
|
||||
]
|
||||
|
||||
|
||||
class StocktakeForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = StockItem
|
||||
|
||||
fields = [
|
||||
'quantity',
|
||||
]
|
||||
|
||||
|
||||
class EditStockItemForm(HelperForm):
|
||||
""" Form for editing a StockItem object.
|
||||
Note that not all fields can be edited here (even if they can be specified during creation.
|
||||
|
@ -22,11 +22,13 @@
|
||||
<ul class="dropdown-menu">
|
||||
{% if item.in_stock %}
|
||||
<li><a href="#" id='stock-edit' title='Edit stock item'>Edit stock item</a></li>
|
||||
<li><a href="#" id='stock-move' title='Move stock item'>Move stock item</a></li>
|
||||
<hr>
|
||||
<li><a href='#' id='stock-add' title='Add stock'>Add to stock</a></li>
|
||||
<li><a href='#' id='stock-remove' title='Remove stock'>Take from stock</a></li>
|
||||
<li><a href='#' id='stock-remove' title='Take stock'>Take from stock</a></li>
|
||||
<li><a href='#' id='stock-stocktake' title='Count stock'>Stocktake</a></li>
|
||||
<li><a href="#" id='stock-move' title='Move stock'>Move stock item</a></li>
|
||||
{% endif %}
|
||||
<hr>
|
||||
<li><a href="#" id='stock-delete' title='Delete stock item'>Delete stock item</a></li>
|
||||
</div>
|
||||
</div>
|
||||
@ -155,40 +157,33 @@
|
||||
});
|
||||
|
||||
{% if item.in_stock %}
|
||||
$("#stock-move").click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'stock-item-move' item.id %}",
|
||||
{
|
||||
reload: true,
|
||||
submit_text: "Move"
|
||||
});
|
||||
});
|
||||
|
||||
function itemAdjust(action) {
|
||||
adjustStock({
|
||||
query: {
|
||||
pk: {{ item.id }},
|
||||
},
|
||||
action: action,
|
||||
success: function() {
|
||||
location.reload();
|
||||
launchModalForm("/stock/adjust/",
|
||||
{
|
||||
data: {
|
||||
action: action,
|
||||
item: {{ item.id }},
|
||||
},
|
||||
reload: true,
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
$("#stock-move").click(function() {
|
||||
itemAdjust("move");
|
||||
});
|
||||
|
||||
$("#stock-stocktake").click(function() {
|
||||
itemAdjust('stocktake');
|
||||
return false;
|
||||
itemAdjust('count');
|
||||
});
|
||||
|
||||
$('#stock-remove').click(function() {
|
||||
itemAdjust('remove');
|
||||
return false;
|
||||
itemAdjust('take');
|
||||
});
|
||||
|
||||
$('#stock-add').click(function() {
|
||||
itemAdjust('add');
|
||||
return false;
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
39
InvenTree/stock/templates/stock/stock_adjust.html
Normal file
39
InvenTree/stock/templates/stock/stock_adjust.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% block pre_form_content %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<input type='hidden' name='stock_action' value='{{ stock_action }}'/>
|
||||
|
||||
<table class='table table-condensed table-striped' id='stock-table'>
|
||||
<tr>
|
||||
<th>Stock Item</th>
|
||||
<th>Location</th>
|
||||
<th>{{ stock_action_title }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for item in stock_items %}
|
||||
<tr id='stock-row-{{ item.id }}' class='error'>
|
||||
<td>{% include "hover_image.html" with image=item.part.image %}
|
||||
{{ item.part.full_name }} <small><i>{{ item.part.description }}</i></small></td>
|
||||
<td>{{ item.location.pathstring }}</td>
|
||||
<td>
|
||||
<input class='numberinput'
|
||||
min='0'
|
||||
{% if stock_action == 'take' or stock_action == 'move' %} max='{{ item.quantity }}' {% endif %}
|
||||
value='{{ item.new_quantity }}' type='number' name='stock-id-{{ item.id }}' id='stock-id-{{ item.id }}'/>
|
||||
{% if item.error %}
|
||||
<br><span class='help-inline'>{{ item.error }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><button class='btn btn-default btn-remove' id='del-{{ item.id }}' title='Remove item' type='button'><span row='stock-row-{{ item.id }}' onclick='removeStockRow()' class='glyphicon glyphicon-small glyphicon-remove'></span></button></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% crispy form %}
|
||||
|
||||
</form>
|
@ -19,8 +19,6 @@ stock_location_detail_urls = [
|
||||
stock_item_detail_urls = [
|
||||
url(r'^edit/?', views.StockItemEdit.as_view(), name='stock-item-edit'),
|
||||
url(r'^delete/?', views.StockItemDelete.as_view(), name='stock-item-delete'),
|
||||
url(r'^move/?', views.StockItemMove.as_view(), name='stock-item-move'),
|
||||
url(r'^stocktake/?', views.StockItemStocktake.as_view(), name='stock-item-stocktake'),
|
||||
url(r'^qr_code/?', views.StockItemQRCode.as_view(), name='stock-item-qr'),
|
||||
|
||||
url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'),
|
||||
@ -36,7 +34,7 @@ stock_urls = [
|
||||
|
||||
url(r'^track/?', views.StockTrackingIndex.as_view(), name='stock-tracking-list'),
|
||||
|
||||
url(r'^move/?', views.StockItemMoveMultiple.as_view(), name='stock-item-move-multiple'),
|
||||
url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'),
|
||||
|
||||
# Individual stock items
|
||||
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),
|
||||
|
@ -10,19 +10,21 @@ from django.views.generic import DetailView, ListView
|
||||
from django.forms.models import model_to_dict
|
||||
from django.forms import HiddenInput
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from InvenTree.views import AjaxView
|
||||
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
|
||||
from InvenTree.views import QRCodeView
|
||||
|
||||
from InvenTree.helpers import str2bool
|
||||
|
||||
from part.models import Part
|
||||
from .models import StockItem, StockLocation, StockItemTracking
|
||||
|
||||
from .forms import EditStockLocationForm
|
||||
from .forms import CreateStockItemForm
|
||||
from .forms import EditStockItemForm
|
||||
from .forms import MoveStockItemForm
|
||||
from .forms import StocktakeForm
|
||||
from .forms import MoveStockItemForm
|
||||
from .forms import AdjustStockForm
|
||||
|
||||
|
||||
class StockIndex(ListView):
|
||||
@ -125,53 +127,274 @@ class StockItemQRCode(QRCodeView):
|
||||
return None
|
||||
|
||||
|
||||
class StockItemMoveMultiple(AjaxView, FormMixin):
|
||||
""" Move multiple stock items """
|
||||
class StockAdjust(AjaxView, FormMixin):
|
||||
""" View for enacting simple stock adjustments:
|
||||
|
||||
- Take items from stock
|
||||
- Add items to stock
|
||||
- Count items
|
||||
- Move stock
|
||||
|
||||
"""
|
||||
|
||||
ajax_template_name = 'stock/stock_move.html'
|
||||
ajax_form_title = 'Move Stock'
|
||||
form_class = MoveStockItemForm
|
||||
ajax_template_name = 'stock/stock_adjust.html'
|
||||
ajax_form_title = 'Adjust Stock'
|
||||
form_class = AdjustStockForm
|
||||
stock_items = []
|
||||
|
||||
def get_items(self, item_list):
|
||||
""" Return list of stock items. """
|
||||
def get_GET_items(self):
|
||||
""" Return list of stock items initally requested using GET """
|
||||
|
||||
items = []
|
||||
# Start with all 'in stock' items
|
||||
items = StockItem.objects.filter(customer=None, belongs_to=None)
|
||||
|
||||
for pk in item_list:
|
||||
try:
|
||||
items.append(StockItem.objects.get(pk=pk))
|
||||
except StockItem.DoesNotExist:
|
||||
pass
|
||||
# Client provides a list of individual stock items
|
||||
if 'stock[]' in self.request.GET:
|
||||
items = items.filter(id__in=self.request.GET.getlist('stock[]'))
|
||||
|
||||
# Client provides a PART reference
|
||||
elif 'part' in self.request.GET:
|
||||
items = items.filter(part=self.request.GET.get('part'))
|
||||
|
||||
# Client provides a LOCATION reference
|
||||
elif 'location' in self.request.GET:
|
||||
items = items.filter(location=self.request.GET.get('location'))
|
||||
|
||||
# Client provides a single StockItem lookup
|
||||
elif 'item' in self.request.GET:
|
||||
items = [StockItem.objects.get(id=self.request.GET.get('item'))]
|
||||
|
||||
# Unsupported query
|
||||
else:
|
||||
items = None
|
||||
|
||||
for item in items:
|
||||
|
||||
# Initialize quantity to zero for addition/removal
|
||||
if self.stock_action in ['take', 'add']:
|
||||
item.new_quantity = 0
|
||||
# Initialize quantity at full amount for counting or moving
|
||||
else:
|
||||
item.new_quantity = item.quantity
|
||||
|
||||
return items
|
||||
|
||||
def get_POST_items(self):
|
||||
""" Return list of stock items sent back by client on a POST request """
|
||||
|
||||
items = []
|
||||
|
||||
for item in self.request.POST:
|
||||
if item.startswith('stock-id-'):
|
||||
|
||||
pk = item.replace('stock-id-', '')
|
||||
q = self.request.POST[item]
|
||||
|
||||
try:
|
||||
stock_item = StockItem.objects.get(pk=pk)
|
||||
except StockItem.DoesNotExist:
|
||||
continue
|
||||
|
||||
stock_item.new_quantity = q
|
||||
|
||||
items.append(stock_item)
|
||||
|
||||
return items
|
||||
|
||||
def get_context_data(self):
|
||||
|
||||
context = super().get_context_data()
|
||||
|
||||
context['stock_items'] = self.stock_items
|
||||
|
||||
context['stock_action'] = self.stock_action
|
||||
|
||||
context['stock_action_title'] = self.stock_action.capitalize()
|
||||
|
||||
return context
|
||||
|
||||
def get_form(self):
|
||||
|
||||
form = super().get_form()
|
||||
|
||||
if not self.stock_action == 'move':
|
||||
form.fields.pop('destination')
|
||||
|
||||
return form
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
item_list = request.GET.getlist('stock[]')
|
||||
|
||||
items = self.get_items(item_list)
|
||||
self.request = request
|
||||
|
||||
print(items)
|
||||
# Action
|
||||
self.stock_action = request.GET.get('action', '').lower()
|
||||
|
||||
return self.renderJsonResponse(request, self.form_class())
|
||||
# Pick a default action...
|
||||
if self.stock_action not in ['move', 'count', 'take', 'add']:
|
||||
self.stock_action = 'count'
|
||||
|
||||
# Choose the form title based on the action
|
||||
titles = {
|
||||
'move': 'Move Stock',
|
||||
'count': 'Count Stock',
|
||||
'take': 'Remove Stock',
|
||||
'add': 'Add Stock'
|
||||
}
|
||||
|
||||
self.ajax_form_title = titles[self.stock_action]
|
||||
|
||||
# Save list of items!
|
||||
self.stock_items = self.get_GET_items()
|
||||
|
||||
return self.renderJsonResponse(request, self.get_form())
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
self.request = request
|
||||
|
||||
self.stock_action = request.POST.get('stock_action').lower()
|
||||
|
||||
# Update list of stock items
|
||||
self.stock_items = self.get_POST_items()
|
||||
|
||||
form = self.get_form()
|
||||
|
||||
valid = form.is_valid()
|
||||
|
||||
for item in self.stock_items:
|
||||
try:
|
||||
item.new_quantity = int(item.new_quantity)
|
||||
except ValueError:
|
||||
item.error = _('Must enter integer value')
|
||||
valid = False
|
||||
continue
|
||||
|
||||
print("Valid:", valid)
|
||||
if item.new_quantity < 0:
|
||||
item.error = _('Quantity must be positive')
|
||||
valid = False
|
||||
continue
|
||||
|
||||
if self.stock_action in ['move', 'take']:
|
||||
|
||||
if item.new_quantity > item.quantity:
|
||||
item.error = _('Quantity must not exceed {x}'.format(x=item.quantity))
|
||||
valid = False
|
||||
continue
|
||||
|
||||
confirmed = str2bool(request.POST.get('confirm'))
|
||||
|
||||
if not confirmed:
|
||||
valid = False
|
||||
form.errors['confirm'] = [_('Confirm stock adjustment')]
|
||||
|
||||
data = {
|
||||
'form_valid': False,
|
||||
'form_valid': valid,
|
||||
}
|
||||
|
||||
#form.errors['note'] = ['hello world']
|
||||
if valid:
|
||||
|
||||
data['success'] = self.do_action()
|
||||
|
||||
return self.renderJsonResponse(request, form, data=data)
|
||||
|
||||
def do_action(self):
|
||||
""" Perform stock adjustment action """
|
||||
|
||||
if self.stock_action == 'move':
|
||||
destination = None
|
||||
|
||||
try:
|
||||
destination = StockLocation.objects.get(id=self.request.POST.get('destination'))
|
||||
except StockLocation.DoesNotExist:
|
||||
pass
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return self.do_move(destination)
|
||||
|
||||
elif self.stock_action == 'add':
|
||||
return self.do_add()
|
||||
|
||||
elif self.stock_action == 'take':
|
||||
return self.do_take()
|
||||
|
||||
elif self.stock_action == 'count':
|
||||
return self.do_count()
|
||||
|
||||
else:
|
||||
return 'No action performed'
|
||||
|
||||
def do_add(self):
|
||||
|
||||
count = 0
|
||||
note = self.request.POST['note']
|
||||
|
||||
for item in self.stock_items:
|
||||
if item.new_quantity <= 0:
|
||||
continue
|
||||
|
||||
item.add_stock(item.new_quantity, self.request.user, notes=note)
|
||||
|
||||
count += 1
|
||||
|
||||
return _("Added stock to {n} items".format(n=count))
|
||||
|
||||
def do_take(self):
|
||||
|
||||
count = 0
|
||||
note = self.request.POST['note']
|
||||
|
||||
for item in self.stock_items:
|
||||
if item.new_quantity <= 0:
|
||||
continue
|
||||
|
||||
item.take_stock(item.new_quantity, self.request.user, notes=note)
|
||||
|
||||
count += 1
|
||||
|
||||
return _("Removed stock from {n} items".format(n=count))
|
||||
|
||||
def do_count(self):
|
||||
|
||||
count = 0
|
||||
note = self.request.POST['note']
|
||||
|
||||
for item in self.stock_items:
|
||||
|
||||
item.stocktake(item.new_quantity, self.request.user, notes=note)
|
||||
|
||||
count += 1
|
||||
|
||||
return _("Counted stock for {n} items".format(n=count))
|
||||
|
||||
def do_move(self, destination):
|
||||
""" Perform actual stock movement """
|
||||
|
||||
count = 0
|
||||
|
||||
note = self.request.POST['note']
|
||||
|
||||
for item in self.stock_items:
|
||||
# Avoid moving zero quantity
|
||||
if item.new_quantity <= 0:
|
||||
continue
|
||||
|
||||
# Do not move to the same location
|
||||
if destination == item.location:
|
||||
continue
|
||||
|
||||
item.move(destination, note, self.request.user, quantity=int(item.new_quantity))
|
||||
|
||||
count += 1
|
||||
|
||||
if count == 0:
|
||||
return _('No items were moved')
|
||||
|
||||
else:
|
||||
return _('Moved {n} items to {dest}'.format(
|
||||
n=count,
|
||||
dest=destination.pathstring))
|
||||
|
||||
|
||||
class StockItemEdit(AjaxUpdateView):
|
||||
"""
|
||||
@ -357,76 +580,6 @@ class StockItemDelete(AjaxDeleteView):
|
||||
ajax_form_title = 'Delete Stock Item'
|
||||
|
||||
|
||||
class StockItemMove(AjaxUpdateView):
|
||||
"""
|
||||
View to move a StockItem from one location to another
|
||||
Performs some data validation to prevent illogical stock moves
|
||||
"""
|
||||
|
||||
model = StockItem
|
||||
ajax_template_name = 'modal_form.html'
|
||||
context_object_name = 'item'
|
||||
ajax_form_title = 'Move Stock Item'
|
||||
form_class = MoveStockItemForm
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.form_class(request.POST, instance=self.get_object())
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
obj = self.get_object()
|
||||
|
||||
try:
|
||||
loc_id = form['location'].value()
|
||||
|
||||
if loc_id:
|
||||
loc = StockLocation.objects.get(pk=form['location'].value())
|
||||
if str(loc.pk) == str(obj.pk):
|
||||
form.errors['location'] = ['Item is already in this location']
|
||||
else:
|
||||
obj.move(loc, form['note'].value(), request.user)
|
||||
else:
|
||||
form.errors['location'] = ['Cannot move to an empty location']
|
||||
|
||||
except StockLocation.DoesNotExist:
|
||||
form.errors['location'] = ['Location does not exist']
|
||||
|
||||
data = {
|
||||
'form_valid': form.is_valid() and len(form.errors) == 0,
|
||||
}
|
||||
|
||||
return self.renderJsonResponse(request, form, data)
|
||||
|
||||
|
||||
class StockItemStocktake(AjaxUpdateView):
|
||||
"""
|
||||
View to perform stocktake on a single StockItem
|
||||
Updates the quantity, which will also create a new StockItemTracking item
|
||||
"""
|
||||
|
||||
model = StockItem
|
||||
template_name = 'modal_form.html'
|
||||
context_object_name = 'item'
|
||||
ajax_form_title = 'Item stocktake'
|
||||
form_class = StocktakeForm
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
form = self.form_class(request.POST, instance=self.get_object())
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
obj = self.get_object()
|
||||
|
||||
obj.stocktake(form.data['quantity'], request.user)
|
||||
|
||||
data = {
|
||||
'form_valid': form.is_valid()
|
||||
}
|
||||
|
||||
return self.renderJsonResponse(request, form, data)
|
||||
|
||||
|
||||
class StockTrackingIndex(ListView):
|
||||
"""
|
||||
StockTrackingIndex provides a page to display StockItemTracking objects
|
||||
|
Reference in New Issue
Block a user