mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master' into match-fields
# Conflicts: # InvenTree/InvenTree/version.py
This commit is contained in:
commit
580effab92
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
patreon: inventree
|
||||||
|
ko_fi: inventree
|
@ -12,14 +12,17 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.6.0 dev"
|
INVENTREE_SW_VERSION = "0.6.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 25
|
INVENTREE_API_VERSION = 26
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
v25 -> 2022-02-16
|
v26 -> 2022-02-17
|
||||||
- Adds API endpoint for uploading a BOM file and extracting data
|
- Adds API endpoint for uploading a BOM file and extracting data
|
||||||
|
|
||||||
|
v25 -> 2022-02-17
|
||||||
|
- Adds ability to filter "part" list endpoint by "in_bom_for" argument
|
||||||
|
|
||||||
v24 -> 2022-02-10
|
v24 -> 2022-02-10
|
||||||
- Adds API endpoint for deleting (cancelling) build order outputs
|
- Adds API endpoint for deleting (cancelling) build order outputs
|
||||||
|
|
||||||
|
@ -208,7 +208,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
raise ValidationError(_("Integer quantity required for trackable parts"))
|
raise ValidationError(_("Integer quantity required for trackable parts"))
|
||||||
|
|
||||||
if part.has_trackable_parts():
|
if part.has_trackable_parts():
|
||||||
raise ValidationError(_("Integer quantity required, as the bill of materials contains tracakble parts"))
|
raise ValidationError(_("Integer quantity required, as the bill of materials contains trackable parts"))
|
||||||
|
|
||||||
return quantity
|
return quantity
|
||||||
|
|
||||||
|
@ -995,6 +995,23 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
except (ValueError, Part.DoesNotExist):
|
except (ValueError, Part.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Filter only parts which are in the "BOM" for a given part
|
||||||
|
in_bom_for = params.get('in_bom_for', None)
|
||||||
|
|
||||||
|
if in_bom_for is not None:
|
||||||
|
try:
|
||||||
|
in_bom_for = Part.objects.get(pk=in_bom_for)
|
||||||
|
|
||||||
|
# Extract a list of parts within the BOM
|
||||||
|
bom_parts = in_bom_for.get_parts_in_bom()
|
||||||
|
print("bom_parts:", bom_parts)
|
||||||
|
print([p.pk for p in bom_parts])
|
||||||
|
|
||||||
|
queryset = queryset.filter(pk__in=[p.pk for p in bom_parts])
|
||||||
|
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
# Filter by whether the BOM has been validated (or not)
|
# Filter by whether the BOM has been validated (or not)
|
||||||
bom_valid = params.get('bom_valid', None)
|
bom_valid = params.get('bom_valid', None)
|
||||||
|
|
||||||
|
@ -483,6 +483,36 @@ class Part(MPTTModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.full_name} - {self.description}"
|
return f"{self.full_name} - {self.description}"
|
||||||
|
|
||||||
|
def get_parts_in_bom(self):
|
||||||
|
"""
|
||||||
|
Return a list of all parts in the BOM for this part.
|
||||||
|
Takes into account substitutes, variant parts, and inherited BOM items
|
||||||
|
"""
|
||||||
|
|
||||||
|
parts = set()
|
||||||
|
|
||||||
|
for bom_item in self.get_bom_items():
|
||||||
|
for part in bom_item.get_valid_parts_for_allocation():
|
||||||
|
parts.add(part)
|
||||||
|
|
||||||
|
return parts
|
||||||
|
|
||||||
|
def check_if_part_in_bom(self, other_part):
|
||||||
|
"""
|
||||||
|
Check if the other_part is in the BOM for this part.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- Accounts for substitute parts
|
||||||
|
- Accounts for variant BOMs
|
||||||
|
"""
|
||||||
|
|
||||||
|
for bom_item in self.get_bom_items():
|
||||||
|
if other_part in bom_item.get_valid_parts_for_allocation():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# No matches found
|
||||||
|
return False
|
||||||
|
|
||||||
def check_add_to_bom(self, parent, raise_error=False, recursive=True):
|
def check_add_to_bom(self, parent, raise_error=False, recursive=True):
|
||||||
"""
|
"""
|
||||||
Check if this Part can be added to the BOM of another part.
|
Check if this Part can be added to the BOM of another part.
|
||||||
|
@ -109,6 +109,31 @@ class StockItemSerialize(generics.CreateAPIView):
|
|||||||
return context
|
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):
|
class StockAdjustView(generics.CreateAPIView):
|
||||||
"""
|
"""
|
||||||
A generic class for handling stocktake actions.
|
A generic class for handling stocktake actions.
|
||||||
@ -503,11 +528,34 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
serial_numbers = data.get('serial_numbers', '')
|
serial_numbers = data.get('serial_numbers', '')
|
||||||
|
|
||||||
# Assign serial numbers for a trackable part
|
# Assign serial numbers for a trackable part
|
||||||
if serial_numbers and part.trackable:
|
if serial_numbers:
|
||||||
|
|
||||||
|
if not part.trackable:
|
||||||
|
raise ValidationError({
|
||||||
|
'serial_numbers': [_("Serial numbers cannot be supplied for a non-trackable part")]
|
||||||
|
})
|
||||||
|
|
||||||
# If serial numbers are specified, check that they match!
|
# If serial numbers are specified, check that they match!
|
||||||
try:
|
try:
|
||||||
serials = extract_serial_numbers(serial_numbers, quantity, part.getLatestSerialNumberInt())
|
serials = extract_serial_numbers(serial_numbers, quantity, part.getLatestSerialNumberInt())
|
||||||
|
|
||||||
|
# Determine if any of the specified serial numbers already exist!
|
||||||
|
existing = []
|
||||||
|
|
||||||
|
for serial in serials:
|
||||||
|
if part.checkIfSerialNumberExists(serial):
|
||||||
|
existing.append(serial)
|
||||||
|
|
||||||
|
if len(existing) > 0:
|
||||||
|
|
||||||
|
msg = _("The following serial numbers already exist")
|
||||||
|
msg += " : "
|
||||||
|
msg += ",".join([str(e) for e in existing])
|
||||||
|
|
||||||
|
raise ValidationError({
|
||||||
|
'serial_numbers': [msg],
|
||||||
|
})
|
||||||
|
|
||||||
except DjangoValidationError as e:
|
except DjangoValidationError as e:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'quantity': e.messages,
|
'quantity': e.messages,
|
||||||
@ -1256,6 +1304,7 @@ stock_api_urls = [
|
|||||||
# Detail views for a single stock item
|
# Detail views for a single stock item
|
||||||
url(r'^(?P<pk>\d+)/', include([
|
url(r'^(?P<pk>\d+)/', include([
|
||||||
url(r'^serialize/', StockItemSerialize.as_view(), name='api-stock-item-serialize'),
|
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'),
|
url(r'^.*$', StockDetail.as_view(), name='api-stock-detail'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ from __future__ import unicode_literals
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.forms.utils import ErrorDict
|
from django.forms.utils import ErrorDict
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
from mptt.fields import TreeNodeChoiceField
|
from mptt.fields import TreeNodeChoiceField
|
||||||
|
|
||||||
@ -16,8 +15,6 @@ from InvenTree.forms import HelperForm
|
|||||||
from InvenTree.fields import RoundingDecimalFormField
|
from InvenTree.fields import RoundingDecimalFormField
|
||||||
from InvenTree.fields import DatePickerFormField
|
from InvenTree.fields import DatePickerFormField
|
||||||
|
|
||||||
from part.models import Part
|
|
||||||
|
|
||||||
from .models import StockLocation, StockItem, StockItemTracking
|
from .models import StockLocation, StockItem, StockItemTracking
|
||||||
|
|
||||||
|
|
||||||
@ -162,56 +159,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):
|
class UninstallStockForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
Form for uninstalling a stock item which is installed in another item.
|
Form for uninstalling a stock item which is installed in another item.
|
||||||
|
@ -391,6 +391,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):
|
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for a simple tree view
|
Serializer for a simple tree view
|
||||||
|
@ -183,16 +183,11 @@
|
|||||||
|
|
||||||
$('#stock-item-install').click(function() {
|
$('#stock-item-install').click(function() {
|
||||||
|
|
||||||
launchModalForm(
|
installStockItem({{ item.pk }}, {{ item.part.pk }}, {
|
||||||
"{% url 'stock-item-install' item.pk %}",
|
onSuccess: function(response) {
|
||||||
{
|
$("#installed-table").bootstrapTable('refresh');
|
||||||
data: {
|
|
||||||
'part': {{ item.part.pk }},
|
|
||||||
'install_item': true,
|
|
||||||
},
|
|
||||||
reload: true,
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
loadInstalledInTable(
|
loadInstalledInTable(
|
||||||
@ -311,65 +306,6 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#test-result-table").on('click', '.button-test-add', function() {
|
|
||||||
var button = $(this);
|
|
||||||
|
|
||||||
var test_name = button.attr('pk');
|
|
||||||
|
|
||||||
constructForm('{% url "api-stock-test-result-list" %}', {
|
|
||||||
method: 'POST',
|
|
||||||
fields: {
|
|
||||||
test: {
|
|
||||||
value: test_name,
|
|
||||||
},
|
|
||||||
result: {},
|
|
||||||
value: {},
|
|
||||||
attachment: {},
|
|
||||||
notes: {},
|
|
||||||
stock_item: {
|
|
||||||
value: {{ item.pk }},
|
|
||||||
hidden: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title: '{% trans "Add Test Result" %}',
|
|
||||||
onSuccess: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#test-result-table").on('click', '.button-test-edit', function() {
|
|
||||||
var button = $(this);
|
|
||||||
|
|
||||||
var pk = button.attr('pk');
|
|
||||||
|
|
||||||
var url = `/api/stock/test/${pk}/`;
|
|
||||||
|
|
||||||
constructForm(url, {
|
|
||||||
fields: {
|
|
||||||
test: {},
|
|
||||||
result: {},
|
|
||||||
value: {},
|
|
||||||
attachment: {},
|
|
||||||
notes: {},
|
|
||||||
},
|
|
||||||
title: '{% trans "Edit Test Result" %}',
|
|
||||||
onSuccess: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#test-result-table").on('click', '.button-test-delete', function() {
|
|
||||||
var button = $(this);
|
|
||||||
|
|
||||||
var pk = button.attr('pk');
|
|
||||||
|
|
||||||
var url = `/api/stock/test/${pk}/`;
|
|
||||||
|
|
||||||
constructForm(url, {
|
|
||||||
method: 'DELETE',
|
|
||||||
title: '{% trans "Delete Test Result" %}',
|
|
||||||
onSuccess: reloadTable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if item.child_count > 0 %}
|
{% if item.child_count > 0 %}
|
||||||
loadStockTable($("#childs-stock-table"), {
|
loadStockTable($("#childs-stock-table"), {
|
||||||
params: {
|
params: {
|
||||||
|
@ -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>
|
<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 %}
|
{% else %}
|
||||||
{% if item.part.get_used_in %}
|
{% 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 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
@ -442,16 +444,7 @@ $("#stock-serialize").click(function() {
|
|||||||
|
|
||||||
$('#stock-install-in').click(function() {
|
$('#stock-install-in').click(function() {
|
||||||
|
|
||||||
launchModalForm(
|
// TODO - Launch dialog to install this item *into* another stock item
|
||||||
"{% url 'stock-item-install' item.pk %}",
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
'part': {{ item.part.pk }},
|
|
||||||
'install_in': true,
|
|
||||||
},
|
|
||||||
reload: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#stock-uninstall').click(function() {
|
$('#stock-uninstall').click(function() {
|
||||||
@ -618,7 +611,7 @@ enableBreadcrumbTree({
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
processNode: function(node) {
|
processNode: function(node) {
|
||||||
node.text = node.name;
|
node.text = node.name;
|
||||||
node.href = `/stock/item/${node.pk}/`;
|
node.href = `/stock/location/${node.pk}/`;
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
@ -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'^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'^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'^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'),
|
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
|
||||||
|
|
||||||
|
@ -465,155 +465,6 @@ class StockItemQRCode(QRCodeView):
|
|||||||
return None
|
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):
|
class StockItemUninstall(AjaxView, FormMixin):
|
||||||
"""
|
"""
|
||||||
View for uninstalling one or more StockItems,
|
View for uninstalling one or more StockItems,
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
editStockLocation,
|
editStockLocation,
|
||||||
exportStock,
|
exportStock,
|
||||||
findStockItemBySerialNumber,
|
findStockItemBySerialNumber,
|
||||||
|
installStockItem,
|
||||||
loadInstalledInTable,
|
loadInstalledInTable,
|
||||||
loadStockAllocationTable,
|
loadStockAllocationTable,
|
||||||
loadStockLocationTable,
|
loadStockLocationTable,
|
||||||
@ -1227,14 +1228,42 @@ function formatDate(row) {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load StockItemTestResult table
|
||||||
|
*/
|
||||||
function loadStockTestResultsTable(table, options) {
|
function loadStockTestResultsTable(table, options) {
|
||||||
/*
|
|
||||||
* Load StockItemTestResult table
|
// Setup filters for the table
|
||||||
*/
|
var filterTarget = options.filterTarget || '#filter-list-stocktests';
|
||||||
|
|
||||||
|
var filterKey = options.filterKey || options.name || 'stocktests';
|
||||||
|
|
||||||
|
var filters = loadTableFilters(filterKey);
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
part: options.part,
|
||||||
|
};
|
||||||
|
|
||||||
|
var original = {};
|
||||||
|
|
||||||
|
for (var k in params) {
|
||||||
|
original[k] = params[k];
|
||||||
|
filters[k] = params[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFilterList(filterKey, table, filterTarget);
|
||||||
|
|
||||||
function makeButtons(row, grouped) {
|
function makeButtons(row, grouped) {
|
||||||
|
|
||||||
|
// Helper function for rendering buttons
|
||||||
|
|
||||||
var html = `<div class='btn-group float-right' role='group'>`;
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
|
if (row.requires_attachment == false && row.requires_value == false && !row.result) {
|
||||||
|
// Enable a "quick tick" option for this test result
|
||||||
|
html += makeIconButton('fa-check-circle icon-green', 'button-test-tick', row.test_name, '{% trans "Pass test" %}');
|
||||||
|
}
|
||||||
|
|
||||||
html += makeIconButton('fa-plus icon-green', 'button-test-add', row.test_name, '{% trans "Add test result" %}');
|
html += makeIconButton('fa-plus icon-green', 'button-test-add', row.test_name, '{% trans "Add test result" %}');
|
||||||
|
|
||||||
if (!grouped && row.result != null) {
|
if (!grouped && row.result != null) {
|
||||||
@ -1258,14 +1287,13 @@ function loadStockTestResultsTable(table, options) {
|
|||||||
rootParentId: parent_node,
|
rootParentId: parent_node,
|
||||||
parentIdField: 'parent',
|
parentIdField: 'parent',
|
||||||
idField: 'pk',
|
idField: 'pk',
|
||||||
uniqueId: 'key',
|
uniqueId: 'pk',
|
||||||
treeShowField: 'test_name',
|
treeShowField: 'test_name',
|
||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
return '{% trans "No test results found" %}';
|
return '{% trans "No test results found" %}';
|
||||||
},
|
},
|
||||||
queryParams: {
|
queryParams: filters,
|
||||||
part: options.part,
|
original: original,
|
||||||
},
|
|
||||||
onPostBody: function() {
|
onPostBody: function() {
|
||||||
table.treegrid({
|
table.treegrid({
|
||||||
treeColumn: 0,
|
treeColumn: 0,
|
||||||
@ -1401,6 +1429,102 @@ function loadStockTestResultsTable(table, options) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Register button callbacks */
|
||||||
|
|
||||||
|
function reloadTestTable(response) {
|
||||||
|
$(table).bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
// "tick" a test result
|
||||||
|
$(table).on('click', '.button-test-tick', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var test_name = button.attr('pk');
|
||||||
|
|
||||||
|
inventreePut(
|
||||||
|
'{% url "api-stock-test-result-list" %}',
|
||||||
|
{
|
||||||
|
test: test_name,
|
||||||
|
result: true,
|
||||||
|
stock_item: options.stock_item,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
success: reloadTestTable,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a test result
|
||||||
|
$(table).on('click', '.button-test-add', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var test_name = button.attr('pk');
|
||||||
|
|
||||||
|
constructForm('{% url "api-stock-test-result-list" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
fields: {
|
||||||
|
test: {
|
||||||
|
value: test_name,
|
||||||
|
},
|
||||||
|
result: {},
|
||||||
|
value: {},
|
||||||
|
attachment: {},
|
||||||
|
notes: {},
|
||||||
|
stock_item: {
|
||||||
|
value: options.stock_item,
|
||||||
|
hidden: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: '{% trans "Add Test Result" %}',
|
||||||
|
onSuccess: reloadTestTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit a test result
|
||||||
|
$(table).on('click', '.button-test-edit', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var pk = button.attr('pk');
|
||||||
|
|
||||||
|
var url = `/api/stock/test/${pk}/`;
|
||||||
|
|
||||||
|
constructForm(url, {
|
||||||
|
fields: {
|
||||||
|
test: {},
|
||||||
|
result: {},
|
||||||
|
value: {},
|
||||||
|
attachment: {},
|
||||||
|
notes: {},
|
||||||
|
},
|
||||||
|
title: '{% trans "Edit Test Result" %}',
|
||||||
|
onSuccess: reloadTestTable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete a test result
|
||||||
|
$(table).on('click', '.button-test-delete', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var pk = button.attr('pk');
|
||||||
|
|
||||||
|
var url = `/api/stock/test/${pk}/`;
|
||||||
|
|
||||||
|
var row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||||
|
|
||||||
|
var html = `
|
||||||
|
<div class='alert alert-block alert-danger'>
|
||||||
|
<strong>{% trans "Delete test result" %}:</strong> ${row.test_name || row.test || row.key}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
constructForm(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
title: '{% trans "Delete Test Result" %}',
|
||||||
|
onSuccess: reloadTestTable,
|
||||||
|
preFormContent: html,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -2837,3 +2961,67 @@ function loadInstalledInTable(table, options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Launch a dialog to install a stock item into another stock item
|
||||||
|
*/
|
||||||
|
function installStockItem(stock_item_id, part_id, options={}) {
|
||||||
|
|
||||||
|
var html = `
|
||||||
|
<div class='alert alert-block alert-info'>
|
||||||
|
<strong>{% trans "Install another stock item into this item" %}</strong><br>
|
||||||
|
{% trans "Stock items can only be installed if they meet the following criteria" %}:<br>
|
||||||
|
<ul>
|
||||||
|
<li>{% trans "The Stock Item links to a Part which is the BOM for this Stock Item" %}</li>
|
||||||
|
<li>{% trans "The Stock Item is currently available in stock" %}</li>
|
||||||
|
<li>{% trans "The Stock Item is serialized and does not belong to another item" %}</li>
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
constructForm(
|
||||||
|
`/api/stock/${stock_item_id}/install/`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
fields: {
|
||||||
|
part: {
|
||||||
|
type: 'related field',
|
||||||
|
required: 'true',
|
||||||
|
label: '{% trans "Part" %}',
|
||||||
|
help_text: '{% trans "Select part to install" %}',
|
||||||
|
model: 'part',
|
||||||
|
api_url: '{% url "api-part-list" %}',
|
||||||
|
auto_fill: true,
|
||||||
|
filters: {
|
||||||
|
trackable: true,
|
||||||
|
in_bom_for: part_id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stock_item: {
|
||||||
|
filters: {
|
||||||
|
part_detail: true,
|
||||||
|
in_stock: true,
|
||||||
|
serialized: true,
|
||||||
|
},
|
||||||
|
adjustFilters: function(filters, opts) {
|
||||||
|
var part = getFormFieldValue('part', {}, opts);
|
||||||
|
|
||||||
|
if (part) {
|
||||||
|
filters.part = part;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirm: true,
|
||||||
|
title: '{% trans "Install Stock Item" %}',
|
||||||
|
preFormContent: html,
|
||||||
|
onSuccess: function(response) {
|
||||||
|
if (options.onSuccess) {
|
||||||
|
options.onSuccess(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -265,12 +265,7 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
|
|
||||||
// Filters for the 'stock test' table
|
// Filters for the 'stock test' table
|
||||||
if (tableKey == 'stocktests') {
|
if (tableKey == 'stocktests') {
|
||||||
return {
|
return {};
|
||||||
result: {
|
|
||||||
type: 'bool',
|
|
||||||
title: '{% trans "Test result" %}',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters for the 'part test template' table
|
// Filters for the 'part test template' table
|
||||||
|
Loading…
Reference in New Issue
Block a user