mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-02 03:30:54 +00:00
Adds API endpoint for installing stock items into other stock items
- Requires more filtering for the Part API - Adds more BOM related functionality for Part model - Removes old server-side form
This commit is contained in:
@ -109,6 +109,31 @@ class StockItemSerialize(generics.CreateAPIView):
|
||||
return context
|
||||
|
||||
|
||||
class StockItemInstall(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for installing a particular stock item into this stock item.
|
||||
|
||||
- stock_item.part must be in the BOM for this part
|
||||
- stock_item must currently be "in stock"
|
||||
- stock_item must be serialized (and not belong to another item)
|
||||
"""
|
||||
|
||||
queryset = StockItem.objects.none()
|
||||
serializer_class = StockSerializers.InstallStockItemSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
context = super().get_serializer_context()
|
||||
context['request'] = self.request
|
||||
|
||||
try:
|
||||
context['item'] = StockItem.objects.get(pk=self.kwargs.get('pk', None))
|
||||
except:
|
||||
pass
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class StockAdjustView(generics.CreateAPIView):
|
||||
"""
|
||||
A generic class for handling stocktake actions.
|
||||
@ -1256,6 +1281,7 @@ stock_api_urls = [
|
||||
# Detail views for a single stock item
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^serialize/', StockItemSerialize.as_view(), name='api-stock-item-serialize'),
|
||||
url(r'^install/', StockItemInstall.as_view(), name='api-stock-item-install'),
|
||||
url(r'^.*$', StockDetail.as_view(), name='api-stock-detail'),
|
||||
])),
|
||||
|
||||
|
@ -162,56 +162,6 @@ class SerializeStockForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class InstallStockForm(HelperForm):
|
||||
"""
|
||||
Form for manually installing a stock item into another stock item
|
||||
|
||||
TODO: Migrate this form to the modern API forms interface
|
||||
"""
|
||||
|
||||
part = forms.ModelChoiceField(
|
||||
queryset=Part.objects.all(),
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
|
||||
stock_item = forms.ModelChoiceField(
|
||||
required=True,
|
||||
queryset=StockItem.objects.filter(StockItem.IN_STOCK_FILTER),
|
||||
help_text=_('Stock item to install')
|
||||
)
|
||||
|
||||
to_install = forms.BooleanField(
|
||||
widget=forms.HiddenInput(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
notes = forms.CharField(
|
||||
required=False,
|
||||
help_text=_('Notes')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = StockItem
|
||||
fields = [
|
||||
'part',
|
||||
'stock_item',
|
||||
# 'quantity_to_install',
|
||||
'notes',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
|
||||
data = super().clean()
|
||||
|
||||
stock_item = data.get('stock_item', None)
|
||||
quantity = data.get('quantity_to_install', None)
|
||||
|
||||
if stock_item and quantity and quantity > stock_item.quantity:
|
||||
raise ValidationError({'quantity_to_install': _('Must not exceed available quantity')})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class UninstallStockForm(forms.ModelForm):
|
||||
"""
|
||||
Form for uninstalling a stock item which is installed in another item.
|
||||
|
@ -36,6 +36,7 @@ import InvenTree.helpers
|
||||
import InvenTree.serializers
|
||||
from InvenTree.serializers import InvenTreeDecimalField, extract_int
|
||||
|
||||
import part.models as part_models
|
||||
from part.serializers import PartBriefSerializer
|
||||
|
||||
|
||||
@ -391,6 +392,63 @@ class SerializeStockItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
|
||||
class InstallStockItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for installing a stock item into a given part
|
||||
"""
|
||||
|
||||
stock_item = serializers.PrimaryKeyRelatedField(
|
||||
queryset=StockItem.objects.all(),
|
||||
many=False,
|
||||
required=True,
|
||||
allow_null=False,
|
||||
label=_('Stock Item'),
|
||||
help_text=_('Select stock item to install'),
|
||||
)
|
||||
|
||||
note = serializers.CharField(
|
||||
label=_('Note'),
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
)
|
||||
|
||||
def validate_stock_item(self, stock_item):
|
||||
"""
|
||||
Validate the selected stock item
|
||||
"""
|
||||
|
||||
if not stock_item.in_stock:
|
||||
# StockItem must be in stock to be "installed"
|
||||
raise ValidationError(_("Stock item is unavailable"))
|
||||
|
||||
# Extract the "parent" item - the item into which the stock item will be installed
|
||||
parent_item = self.context['item']
|
||||
parent_part = parent_item.part
|
||||
|
||||
if not parent_part.check_if_part_in_bom(stock_item.part):
|
||||
raise ValidationError(_("Selected part is not in the Bill of Materials"))
|
||||
|
||||
return stock_item
|
||||
|
||||
def save(self):
|
||||
""" Install the selected stock item into this one """
|
||||
|
||||
data = self.validated_data
|
||||
|
||||
stock_item = data['stock_item']
|
||||
note = data.get('note', '')
|
||||
|
||||
parent_item = self.context['item']
|
||||
request = self.context['request']
|
||||
|
||||
parent_item.installStockItem(
|
||||
stock_item,
|
||||
stock_item.quantity,
|
||||
request.user,
|
||||
note,
|
||||
)
|
||||
|
||||
|
||||
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializer for a simple tree view
|
||||
|
@ -183,16 +183,11 @@
|
||||
|
||||
$('#stock-item-install').click(function() {
|
||||
|
||||
launchModalForm(
|
||||
"{% url 'stock-item-install' item.pk %}",
|
||||
{
|
||||
data: {
|
||||
'part': {{ item.part.pk }},
|
||||
'install_item': true,
|
||||
},
|
||||
reload: true,
|
||||
installStockItem({{ item.pk }}, {{ item.part.pk }}, {
|
||||
onSuccess: function(response) {
|
||||
$("#installed-table").bootstrapTable('refresh');
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
loadInstalledInTable(
|
||||
|
@ -98,7 +98,9 @@
|
||||
<li><a class='dropdown-item' href='#' id='stock-uninstall' title='{% trans "Uninstall stock item" %}'><span class='fas fa-unlink'></span> {% trans "Uninstall" %}</a></li>
|
||||
{% else %}
|
||||
{% if item.part.get_used_in %}
|
||||
<li><a class='dropdown-item' href='#' id='stock-install-in' title='{% trans "Install stock item" %}'><span class='fas fa-link'></span> {% trans "Install" %}</a></li>
|
||||
<!--
|
||||
<li><a class='dropdown-item' href='#' id='stock-install-in' title='{% trans "Install stock item" %}'><span class='fas fa-link'></span> {% trans "Install" %}</a></li>
|
||||
-->
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
@ -442,16 +444,7 @@ $("#stock-serialize").click(function() {
|
||||
|
||||
$('#stock-install-in').click(function() {
|
||||
|
||||
launchModalForm(
|
||||
"{% url 'stock-item-install' item.pk %}",
|
||||
{
|
||||
data: {
|
||||
'part': {{ item.part.pk }},
|
||||
'install_in': true,
|
||||
},
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
// TODO - Launch dialog to install this item *into* another stock item
|
||||
});
|
||||
|
||||
$('#stock-uninstall').click(function() {
|
||||
|
@ -1,33 +0,0 @@
|
||||
{% extends "modal_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
{% if install_item %}
|
||||
<p>
|
||||
{% trans "Install another Stock Item into this item." %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Stock items can only be installed if they meet the following criteria" %}:
|
||||
|
||||
<ul>
|
||||
<li>{% trans "The Stock Item links to a Part which is in the BOM for this Stock Item" %}</li>
|
||||
<li>{% trans "The Stock Item is currently in stock" %}</li>
|
||||
<li>{% trans "The Stock Item is serialized and does not belong to another item" %}</li>
|
||||
</ul>
|
||||
</p>
|
||||
{% elif install_in %}
|
||||
<p>
|
||||
{% trans "Install this Stock Item in another stock item." %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Stock items can only be installed if they meet the following criteria" %}:
|
||||
|
||||
<ul>
|
||||
<li>{% trans "The part associated to this Stock Item belongs to another part's BOM" %}</li>
|
||||
<li>{% trans "This Stock Item is serialized and does not belong to another item" %}</li>
|
||||
</ul>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -24,7 +24,6 @@ stock_item_detail_urls = [
|
||||
url(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'),
|
||||
url(r'^delete_test_data/', views.StockItemDeleteTestData.as_view(), name='stock-item-delete-test-data'),
|
||||
url(r'^return/', views.StockItemReturnToStock.as_view(), name='stock-item-return'),
|
||||
url(r'^install/', views.StockItemInstall.as_view(), name='stock-item-install'),
|
||||
|
||||
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
|
||||
|
||||
|
@ -465,155 +465,6 @@ class StockItemQRCode(QRCodeView):
|
||||
return None
|
||||
|
||||
|
||||
class StockItemInstall(AjaxUpdateView):
|
||||
"""
|
||||
View for manually installing stock items into
|
||||
a particular stock item.
|
||||
|
||||
In contrast to the StockItemUninstall view,
|
||||
only a single stock item can be installed at once.
|
||||
|
||||
The "part" to be installed must be provided in the GET query parameters.
|
||||
|
||||
"""
|
||||
|
||||
model = StockItem
|
||||
form_class = StockForms.InstallStockForm
|
||||
ajax_form_title = _('Install Stock Item')
|
||||
ajax_template_name = "stock/item_install.html"
|
||||
|
||||
part = None
|
||||
|
||||
def get_params(self):
|
||||
""" Retrieve GET parameters """
|
||||
|
||||
# Look at GET params
|
||||
self.part_id = self.request.GET.get('part', None)
|
||||
self.install_in = self.request.GET.get('install_in', False)
|
||||
self.install_item = self.request.GET.get('install_item', False)
|
||||
|
||||
if self.part_id is None:
|
||||
# Look at POST params
|
||||
self.part_id = self.request.POST.get('part', None)
|
||||
|
||||
try:
|
||||
self.part = Part.objects.get(pk=self.part_id)
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
self.part = None
|
||||
|
||||
def get_stock_items(self):
|
||||
"""
|
||||
Return a list of stock items suitable for displaying to the user.
|
||||
|
||||
Requirements:
|
||||
- Items must be in stock
|
||||
- Items must be in BOM of stock item
|
||||
- Items must be serialized
|
||||
"""
|
||||
|
||||
# Filter items in stock
|
||||
items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER)
|
||||
|
||||
# Filter serialized stock items
|
||||
items = items.exclude(serial__isnull=True).exclude(serial__exact='')
|
||||
|
||||
if self.part:
|
||||
# Filter for parts to install this item in
|
||||
if self.install_in:
|
||||
# Get parts using this part
|
||||
allowed_parts = self.part.get_used_in()
|
||||
# Filter
|
||||
items = items.filter(part__in=allowed_parts)
|
||||
|
||||
# Filter for parts to install in this item
|
||||
if self.install_item:
|
||||
# Get all parts which can be installed into this part
|
||||
allowed_parts = self.part.get_installed_part_options()
|
||||
# Filter
|
||||
items = items.filter(part__in=allowed_parts)
|
||||
|
||||
return items
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Retrieve parameters and update context """
|
||||
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
# Get request parameters
|
||||
self.get_params()
|
||||
|
||||
ctx.update({
|
||||
'part': self.part,
|
||||
'install_in': self.install_in,
|
||||
'install_item': self.install_item,
|
||||
})
|
||||
|
||||
return ctx
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initials = super().get_initial()
|
||||
|
||||
items = self.get_stock_items()
|
||||
|
||||
# If there is a single stock item available, we can use it!
|
||||
if items.count() == 1:
|
||||
item = items.first()
|
||||
initials['stock_item'] = item.pk
|
||||
|
||||
if self.part:
|
||||
initials['part'] = self.part
|
||||
|
||||
try:
|
||||
# Is this stock item being installed in the other stock item?
|
||||
initials['to_install'] = self.install_in or not self.install_item
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return initials
|
||||
|
||||
def get_form(self):
|
||||
|
||||
form = super().get_form()
|
||||
|
||||
form.fields['stock_item'].queryset = self.get_stock_items()
|
||||
|
||||
return form
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
self.get_params()
|
||||
|
||||
form = self.get_form()
|
||||
|
||||
valid = form.is_valid()
|
||||
|
||||
if valid:
|
||||
# We assume by this point that we have a valid stock_item and quantity values
|
||||
data = form.cleaned_data
|
||||
|
||||
other_stock_item = data['stock_item']
|
||||
# Quantity will always be 1 for serialized item
|
||||
quantity = 1
|
||||
notes = data['notes']
|
||||
|
||||
# Get stock item
|
||||
this_stock_item = self.get_object()
|
||||
|
||||
if data['to_install']:
|
||||
# Install this stock item into the other stock item
|
||||
other_stock_item.installStockItem(this_stock_item, quantity, request.user, notes)
|
||||
else:
|
||||
# Install the other stock item into this one
|
||||
this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes)
|
||||
|
||||
data = {
|
||||
'form_valid': valid,
|
||||
}
|
||||
|
||||
return self.renderJsonResponse(request, form, data=data)
|
||||
|
||||
|
||||
class StockItemUninstall(AjaxView, FormMixin):
|
||||
"""
|
||||
View for uninstalling one or more StockItems,
|
||||
|
Reference in New Issue
Block a user