mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 13:06:45 +00:00
Merge pull request #1019 from SchrodingersGat/installed-stock-improvements
Improvements for the "Installed Items" tab for StockItem display
This commit is contained in:
commit
7f3ce9b0b1
@ -105,9 +105,14 @@ function makeProgressBar(value, maximum, opts) {
|
|||||||
var options = opts || {};
|
var options = opts || {};
|
||||||
|
|
||||||
value = parseFloat(value);
|
value = parseFloat(value);
|
||||||
maximum = parseFloat(maximum);
|
|
||||||
|
|
||||||
var percent = parseInt(value / maximum * 100);
|
var percent = 100;
|
||||||
|
|
||||||
|
// Prevent div-by-zero or null value
|
||||||
|
if (maximum && maximum > 0) {
|
||||||
|
maximum = parseFloat(maximum);
|
||||||
|
percent = parseInt(value / maximum * 100);
|
||||||
|
}
|
||||||
|
|
||||||
if (percent > 100) {
|
if (percent > 100) {
|
||||||
percent = 100;
|
percent = 100;
|
||||||
@ -115,18 +120,28 @@ function makeProgressBar(value, maximum, opts) {
|
|||||||
|
|
||||||
var extraclass = '';
|
var extraclass = '';
|
||||||
|
|
||||||
if (value > maximum) {
|
if (maximum) {
|
||||||
|
// TODO - Special color?
|
||||||
|
}
|
||||||
|
else if (value > maximum) {
|
||||||
extraclass='progress-bar-over';
|
extraclass='progress-bar-over';
|
||||||
} else if (value < maximum) {
|
} else if (value < maximum) {
|
||||||
extraclass = 'progress-bar-under';
|
extraclass = 'progress-bar-under';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var text = value;
|
||||||
|
|
||||||
|
if (maximum) {
|
||||||
|
text += ' / ';
|
||||||
|
text += maximum;
|
||||||
|
}
|
||||||
|
|
||||||
var id = options.id || 'progress-bar';
|
var id = options.id || 'progress-bar';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div id='${id}' class='progress'>
|
<div id='${id}' class='progress'>
|
||||||
<div class='progress-bar ${extraclass}' role='progressbar' aria-valuenow='${percent}' aria-valuemin='0' aria-valuemax='100' style='width:${percent}%'></div>
|
<div class='progress-bar ${extraclass}' role='progressbar' aria-valuenow='${percent}' aria-valuemin='0' aria-valuemax='100' style='width:${percent}%'></div>
|
||||||
<div class='progress-value'>${value} / ${maximum}</div>
|
<div class='progress-value'>${text}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -109,10 +109,20 @@ $.fn.inventreeTable = function(options) {
|
|||||||
options.pagination = true;
|
options.pagination = true;
|
||||||
options.pageSize = inventreeLoad(varName, 25);
|
options.pageSize = inventreeLoad(varName, 25);
|
||||||
options.pageList = [25, 50, 100, 250, 'all'];
|
options.pageList = [25, 50, 100, 250, 'all'];
|
||||||
|
|
||||||
options.rememberOrder = true;
|
options.rememberOrder = true;
|
||||||
|
|
||||||
|
if (options.sortable == null) {
|
||||||
options.sortable = true;
|
options.sortable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.search == null) {
|
||||||
options.search = true;
|
options.search = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.showColumns == null) {
|
||||||
options.showColumns = true;
|
options.showColumns = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Callback to save pagination data
|
// Callback to save pagination data
|
||||||
options.onPageChange = function(number, size) {
|
options.onPageChange = function(number, size) {
|
||||||
|
@ -777,6 +777,13 @@ class BomList(generics.ListCreateAPIView):
|
|||||||
if sub_part is not None:
|
if sub_part is not None:
|
||||||
queryset = queryset.filter(sub_part=sub_part)
|
queryset = queryset.filter(sub_part=sub_part)
|
||||||
|
|
||||||
|
# Filter by "trackable" status of the sub-part
|
||||||
|
trackable = self.request.query_params.get('trackable', None)
|
||||||
|
|
||||||
|
if trackable is not None:
|
||||||
|
trackable = str2bool(trackable)
|
||||||
|
queryset = queryset.filter(sub_part__trackable=trackable)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
|
@ -8,6 +8,8 @@ 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 as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.core.validators import MinValueValidator
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from mptt.fields import TreeNodeChoiceField
|
from mptt.fields import TreeNodeChoiceField
|
||||||
|
|
||||||
@ -17,6 +19,8 @@ from InvenTree.fields import RoundingDecimalFormField
|
|||||||
|
|
||||||
from report.models import TestReport
|
from report.models import TestReport
|
||||||
|
|
||||||
|
from part.models import Part
|
||||||
|
|
||||||
from .models import StockLocation, StockItem, StockItemTracking
|
from .models import StockLocation, StockItem, StockItemTracking
|
||||||
from .models import StockItemAttachment
|
from .models import StockItemAttachment
|
||||||
from .models import StockItemTestResult
|
from .models import StockItemTestResult
|
||||||
@ -271,6 +275,59 @@ class ExportOptionsForm(HelperForm):
|
|||||||
self.fields['file_format'].choices = self.get_format_choices()
|
self.fields['file_format'].choices = self.get_format_choices()
|
||||||
|
|
||||||
|
|
||||||
|
class InstallStockForm(HelperForm):
|
||||||
|
"""
|
||||||
|
Form for manually installing a stock item into another stock item
|
||||||
|
"""
|
||||||
|
|
||||||
|
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')
|
||||||
|
)
|
||||||
|
|
||||||
|
quantity_to_install = RoundingDecimalFormField(
|
||||||
|
max_digits=10, decimal_places=5,
|
||||||
|
initial=1,
|
||||||
|
label=_('Quantity'),
|
||||||
|
help_text=_('Stock quantity to assign'),
|
||||||
|
validators=[
|
||||||
|
MinValueValidator(0.001)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
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.
|
||||||
|
@ -600,12 +600,13 @@ class StockItem(MPTTModel):
|
|||||||
return self.installedItemCount() > 0
|
return self.installedItemCount() > 0
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def installIntoStockItem(self, otherItem, user, notes):
|
def installStockItem(self, otherItem, quantity, user, notes):
|
||||||
"""
|
"""
|
||||||
Install this stock item into another stock item.
|
Install another stock item into this stock item.
|
||||||
|
|
||||||
Args
|
Args
|
||||||
otherItem: The stock item to install this item into
|
otherItem: The stock item to install into this stock item
|
||||||
|
quantity: The quantity of stock to install
|
||||||
user: The user performing the operation
|
user: The user performing the operation
|
||||||
notes: Any notes associated with the operation
|
notes: Any notes associated with the operation
|
||||||
"""
|
"""
|
||||||
@ -614,18 +615,29 @@ class StockItem(MPTTModel):
|
|||||||
if self.belongs_to is not None:
|
if self.belongs_to is not None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# TODO - Are there any other checks that need to be performed at this stage?
|
# If the quantity is less than the stock item, split the stock!
|
||||||
|
stock_item = otherItem.splitStock(quantity, None, user)
|
||||||
|
|
||||||
# Mark this stock item as belonging to the other one
|
if stock_item is None:
|
||||||
self.belongs_to = otherItem
|
stock_item = otherItem
|
||||||
|
|
||||||
self.save()
|
# Assign the other stock item into this one
|
||||||
|
stock_item.belongs_to = self
|
||||||
|
stock_item.save()
|
||||||
|
|
||||||
# Add a transaction note!
|
# Add a transaction note to the other item
|
||||||
self.addTransactionNote(
|
stock_item.addTransactionNote(
|
||||||
_('Installed in stock item') + ' ' + str(otherItem.pk),
|
_('Installed into stock item') + ' ' + str(self.pk),
|
||||||
user,
|
user,
|
||||||
notes=notes
|
notes=notes,
|
||||||
|
url=self.get_absolute_url()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a transaction note to this item
|
||||||
|
self.addTransactionNote(
|
||||||
|
_('Installed stock item') + ' ' + str(stock_item.pk),
|
||||||
|
user, notes=notes,
|
||||||
|
url=stock_item.get_absolute_url()
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@ -645,16 +657,31 @@ class StockItem(MPTTModel):
|
|||||||
|
|
||||||
# TODO - Are there any other checks that need to be performed at this stage?
|
# TODO - Are there any other checks that need to be performed at this stage?
|
||||||
|
|
||||||
|
# Add a transaction note to the parent item
|
||||||
|
self.belongs_to.addTransactionNote(
|
||||||
|
_("Uninstalled stock item") + ' ' + str(self.pk),
|
||||||
|
user,
|
||||||
|
notes=notes,
|
||||||
|
url=self.get_absolute_url(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mark this stock item as *not* belonging to anyone
|
||||||
self.belongs_to = None
|
self.belongs_to = None
|
||||||
self.location = location
|
self.location = location
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
if location:
|
||||||
|
url = location.get_absolute_url()
|
||||||
|
else:
|
||||||
|
url = ''
|
||||||
|
|
||||||
# Add a transaction note!
|
# Add a transaction note!
|
||||||
self.addTransactionNote(
|
self.addTransactionNote(
|
||||||
_('Uninstalled into location') + ' ' + str(location),
|
_('Uninstalled into location') + ' ' + str(location),
|
||||||
user,
|
user,
|
||||||
notes=notes
|
notes=notes,
|
||||||
|
url=url
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -838,20 +865,20 @@ class StockItem(MPTTModel):
|
|||||||
|
|
||||||
# Do not split a serialized part
|
# Do not split a serialized part
|
||||||
if self.serialized:
|
if self.serialized:
|
||||||
return
|
return self
|
||||||
|
|
||||||
try:
|
try:
|
||||||
quantity = Decimal(quantity)
|
quantity = Decimal(quantity)
|
||||||
except (InvalidOperation, ValueError):
|
except (InvalidOperation, ValueError):
|
||||||
return
|
return self
|
||||||
|
|
||||||
# Doesn't make sense for a zero quantity
|
# Doesn't make sense for a zero quantity
|
||||||
if quantity <= 0:
|
if quantity <= 0:
|
||||||
return
|
return self
|
||||||
|
|
||||||
# Also doesn't make sense to split the full amount
|
# Also doesn't make sense to split the full amount
|
||||||
if quantity >= self.quantity:
|
if quantity >= self.quantity:
|
||||||
return
|
return self
|
||||||
|
|
||||||
# Create a new StockItem object, duplicating relevant fields
|
# Create a new StockItem object, duplicating relevant fields
|
||||||
# Nullify the PK so a new record is created
|
# Nullify the PK so a new record is created
|
||||||
|
17
InvenTree/stock/templates/stock/item_install.html
Normal file
17
InvenTree/stock/templates/stock/item_install.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% extends "modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block pre_form_content %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% trans "Install another StockItem into this item." %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% trans "Stock items can only be installed if they meet the following criteria" %}:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>{% trans "The StockItem links to a Part which is in the BOM for this StockItem" %}</li>
|
||||||
|
<li>{% trans "The StockItem is currently in stock" %}</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
@ -10,19 +10,7 @@
|
|||||||
<h4>{% trans "Installed Stock Items" %}</h4>
|
<h4>{% trans "Installed Stock Items" %}</h4>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div id='button-toolbar'>
|
<table class='table table-striped table-condensed' id='installed-table'></table>
|
||||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li><a href="#" id='multi-item-uninstall' title='{% trans "Uninstall selected stock items" %}'>{% trans "Uninstall" %}</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='installed-table' data-toolbar='#button-toolbar'>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -30,135 +18,14 @@
|
|||||||
|
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
$('#installed-table').inventreeTable({
|
loadInstalledInTable(
|
||||||
formatNoMatches: function() {
|
$('#installed-table'),
|
||||||
return '{% trans "No stock items installed" %}';
|
|
||||||
},
|
|
||||||
url: "{% url 'api-stock-list' %}",
|
|
||||||
queryParams: {
|
|
||||||
installed_in: {{ item.id }},
|
|
||||||
part_detail: true,
|
|
||||||
},
|
|
||||||
name: 'stock-item-installed',
|
|
||||||
url: "{% url 'api-stock-list' %}",
|
|
||||||
showColumns: true,
|
|
||||||
columns: [
|
|
||||||
{
|
{
|
||||||
checkbox: true,
|
stock_item: {{ item.pk }},
|
||||||
title: '{% trans 'Select' %}',
|
part: {{ item.part.pk }},
|
||||||
searchable: false,
|
quantity: {{ item.quantity }},
|
||||||
switchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
title: 'ID',
|
|
||||||
visible: false,
|
|
||||||
switchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'part_name',
|
|
||||||
title: '{% trans "Part" %}',
|
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
|
|
||||||
var url = `/stock/item/${row.pk}/`;
|
|
||||||
var thumb = row.part_detail.thumbnail;
|
|
||||||
var name = row.part_detail.full_name;
|
|
||||||
|
|
||||||
html = imageHoverIcon(thumb) + renderLink(name, url);
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
}
|
||||||
},
|
);
|
||||||
{
|
|
||||||
field: 'IPN',
|
|
||||||
title: 'IPN',
|
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return row.part_detail.IPN;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'part_description',
|
|
||||||
title: '{% trans "Description" %}',
|
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return row.part_detail.description;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'quantity',
|
|
||||||
title: '{% trans "Stock" %}',
|
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
|
|
||||||
var val = parseFloat(value);
|
|
||||||
|
|
||||||
// If there is a single unit with a serial number, use the serial number
|
|
||||||
if (row.serial && row.quantity == 1) {
|
|
||||||
val = '# ' + row.serial;
|
|
||||||
} else {
|
|
||||||
val = +val.toFixed(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
var html = renderLink(val, `/stock/item/${row.pk}/`);
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'status',
|
|
||||||
title: '{% trans "Status" %}',
|
|
||||||
sortable: 'true',
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return stockStatusDisplay(value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'batch',
|
|
||||||
title: '{% trans "Batch" %}',
|
|
||||||
sortable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'actions',
|
|
||||||
switchable: false,
|
|
||||||
title: '',
|
|
||||||
formatter: function(value, row) {
|
|
||||||
var pk = row.pk;
|
|
||||||
|
|
||||||
var html = `<div class='btn-group float-right' role='group'>`;
|
|
||||||
|
|
||||||
html += makeIconButton('fa-unlink', 'button-uninstall', pk, '{% trans "Uninstall item" %}');
|
|
||||||
|
|
||||||
html += `</div>`;
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onLoadSuccess: function() {
|
|
||||||
|
|
||||||
var table = $('#installed-table');
|
|
||||||
|
|
||||||
// Find buttons and associate actions
|
|
||||||
table.find('.button-uninstall').click(function() {
|
|
||||||
var pk = $(this).attr('pk');
|
|
||||||
|
|
||||||
launchModalForm(
|
|
||||||
"{% url 'stock-item-uninstall' %}",
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
'items[]': [pk],
|
|
||||||
},
|
|
||||||
reload: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
buttons: [
|
|
||||||
'#stock-options',
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#multi-item-uninstall').click(function() {
|
$('#multi-item-uninstall').click(function() {
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
{% extends "modal_form.html" %}
|
{% extends "modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
Create serialized items from this stock item.<br>
|
{% trans "Create serialized items from this stock item." %}
|
||||||
Select quantity to serialize, and unique serial numbers.
|
<br>
|
||||||
|
{% trans "Select quantity to serialize, and unique serial numbers." %}
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -25,6 +25,7 @@ stock_item_detail_urls = [
|
|||||||
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'^assign/', views.StockItemAssignToCustomer.as_view(), name='stock-item-assign'),
|
url(r'^assign/', views.StockItemAssignToCustomer.as_view(), name='stock-item-assign'),
|
||||||
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'),
|
||||||
|
|
||||||
|
@ -683,6 +683,106 @@ 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_stock_items(self):
|
||||||
|
"""
|
||||||
|
Return a list of stock items suitable for displaying to the user.
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- Items must be in stock
|
||||||
|
|
||||||
|
Filters:
|
||||||
|
- Items can be filtered by Part reference
|
||||||
|
"""
|
||||||
|
|
||||||
|
items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER)
|
||||||
|
|
||||||
|
# Filter by Part association
|
||||||
|
|
||||||
|
# Look at GET params
|
||||||
|
part_id = self.request.GET.get('part', None)
|
||||||
|
|
||||||
|
if part_id is None:
|
||||||
|
# Look at POST params
|
||||||
|
part_id = self.request.POST.get('part', None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.part = Part.objects.get(pk=part_id)
|
||||||
|
items = items.filter(part=self.part)
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
self.part = None
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
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
|
||||||
|
initials['quantity_to_install'] = item.quantity
|
||||||
|
|
||||||
|
if self.part:
|
||||||
|
initials['part'] = self.part
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
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 = data['quantity_to_install']
|
||||||
|
notes = data['notes']
|
||||||
|
|
||||||
|
# Install the other stock item into this one
|
||||||
|
this_stock_item = self.get_object()
|
||||||
|
|
||||||
|
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,
|
||||||
|
@ -470,11 +470,17 @@ function loadStockTable(table, options) {
|
|||||||
|
|
||||||
if (row.customer) {
|
if (row.customer) {
|
||||||
html += `<span class='fas fa-user-tie label-right' title='{% trans "Stock item has been assigned to customer" %}'></span>`;
|
html += `<span class='fas fa-user-tie label-right' title='{% trans "Stock item has been assigned to customer" %}'></span>`;
|
||||||
} else if (row.build_order) {
|
} else {
|
||||||
|
if (row.build_order) {
|
||||||
html += `<span class='fas fa-tools label-right' title='{% trans "Stock item was assigned to a build order" %}'></span>`;
|
html += `<span class='fas fa-tools label-right' title='{% trans "Stock item was assigned to a build order" %}'></span>`;
|
||||||
} else if (row.sales_order) {
|
} else if (row.sales_order) {
|
||||||
html += `<span class='fas fa-dollar-sign label-right' title='{% trans "Stock item was assigned to a sales order" %}'></span>`;
|
html += `<span class='fas fa-dollar-sign label-right' title='{% trans "Stock item was assigned to a sales order" %}'></span>`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.belongs_to) {
|
||||||
|
html += `<span class='fas fa-box label-right' title='{% trans "Stock item has been installed in another item" %}'></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
// Special stock status codes
|
// Special stock status codes
|
||||||
|
|
||||||
@ -520,6 +526,9 @@ function loadStockTable(table, options) {
|
|||||||
} else if (row.customer) {
|
} else if (row.customer) {
|
||||||
var text = "{% trans "Shipped to customer" %}";
|
var text = "{% trans "Shipped to customer" %}";
|
||||||
return renderLink(text, `/company/${row.customer}/assigned-stock/`);
|
return renderLink(text, `/company/${row.customer}/assigned-stock/`);
|
||||||
|
} else if (row.sales_order) {
|
||||||
|
var text = `{% trans "Assigned to sales order" %}`;
|
||||||
|
return renderLink(text, `/order/sales-order/${row.sales_order}/`);
|
||||||
}
|
}
|
||||||
else if (value) {
|
else if (value) {
|
||||||
return renderLink(value, `/stock/location/${row.location}/`);
|
return renderLink(value, `/stock/location/${row.location}/`);
|
||||||
@ -799,3 +808,300 @@ function createNewStockItem(options) {
|
|||||||
|
|
||||||
launchModalForm("{% url 'stock-item-create' %}", options);
|
launchModalForm("{% url 'stock-item-create' %}", options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadInstalledInTable(table, options) {
|
||||||
|
/*
|
||||||
|
* Display a table showing the stock items which are installed in this stock item.
|
||||||
|
* This is a multi-level tree table, where the "top level" items are Part objects,
|
||||||
|
* and the children of each top-level item are the associated installed stock items.
|
||||||
|
*
|
||||||
|
* The process for retrieving data and displaying the table is as follows:
|
||||||
|
*
|
||||||
|
* A) Get BOM data for the stock item
|
||||||
|
* - It is assumed that the stock item will be for an assembly
|
||||||
|
* (otherwise why are we installing stuff anyway?)
|
||||||
|
* - Request BOM items for stock_item.part (and only for trackable sub items)
|
||||||
|
*
|
||||||
|
* B) Add parts to table
|
||||||
|
* - Create rows for each trackable sub-part in the table
|
||||||
|
*
|
||||||
|
* C) Gather installed stock item data
|
||||||
|
* - Get the list of installed stock items via the API
|
||||||
|
* - If the Part reference is already in the table, add the sub-item as a child
|
||||||
|
* - If this is a stock item for a *new* part, request that part from the API,
|
||||||
|
* and add that part as a new row, then add the stock item as a child of that part
|
||||||
|
*
|
||||||
|
* D) Enjoy!
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* And the options object contains the following things:
|
||||||
|
*
|
||||||
|
* - stock_item: The PK of the master stock_item object
|
||||||
|
* - part: The PK of the Part reference of the stock_item object
|
||||||
|
* - quantity: The quantity of the stock item
|
||||||
|
*/
|
||||||
|
|
||||||
|
function updateCallbacks() {
|
||||||
|
// Setup callback functions when buttons are pressed
|
||||||
|
table.find('.button-install').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
launchModalForm(
|
||||||
|
`/stock/item/${options.stock_item}/install/`,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
part: pk,
|
||||||
|
},
|
||||||
|
success: function() {
|
||||||
|
// Refresh entire table!
|
||||||
|
table.bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
table.inventreeTable(
|
||||||
|
{
|
||||||
|
url: "{% url 'api-bom-list' %}",
|
||||||
|
queryParams: {
|
||||||
|
part: options.part,
|
||||||
|
trackable: true,
|
||||||
|
sub_part_detail: true,
|
||||||
|
},
|
||||||
|
showColumns: false,
|
||||||
|
name: 'installed-in',
|
||||||
|
detailView: true,
|
||||||
|
detailViewByClick: true,
|
||||||
|
detailFilter: function(index, row) {
|
||||||
|
return row.installed_count && row.installed_count > 0;
|
||||||
|
},
|
||||||
|
detailFormatter: function(index, row, element) {
|
||||||
|
var subTableId = `installed-table-${row.sub_part}`;
|
||||||
|
|
||||||
|
var html = `<div class='sub-table'><table class='table table-condensed table-striped' id='${subTableId}'></table></div>`;
|
||||||
|
|
||||||
|
element.html(html);
|
||||||
|
|
||||||
|
var subTable = $(`#${subTableId}`);
|
||||||
|
|
||||||
|
// Display a "sub table" showing all the linked stock items
|
||||||
|
subTable.bootstrapTable({
|
||||||
|
data: row.installed_items,
|
||||||
|
showHeader: true,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'item',
|
||||||
|
title: '{% trans "Stock Item" %}',
|
||||||
|
formatter: function(value, subrow, index, field) {
|
||||||
|
|
||||||
|
var pk = subrow.pk;
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
if (subrow.serial && subrow.quantity == 1) {
|
||||||
|
html += `{% trans "Serial" %}: ${subrow.serial}`;
|
||||||
|
} else {
|
||||||
|
html += `{% trans "Quantity" %}: ${subrow.quantity}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderLink(html, `/stock/item/${subrow.pk}/`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '{% trans "Status" %}',
|
||||||
|
formatter: function(value, subrow, index, field) {
|
||||||
|
return stockStatusDisplay(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'batch',
|
||||||
|
title: '{% trans "Batch" %}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
title: '',
|
||||||
|
formatter: function(value, subrow, index) {
|
||||||
|
|
||||||
|
var pk = subrow.pk;
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
// Add some buttons yo!
|
||||||
|
html += `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
|
html += makeIconButton('fa-unlink', 'button-uninstall', pk, "{% trans "Uninstall stock item" %}");
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
onPostBody: function() {
|
||||||
|
// Setup button callbacks
|
||||||
|
subTable.find('.button-uninstall').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'stock-item-uninstall' %}",
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
'items[]': [pk],
|
||||||
|
},
|
||||||
|
success: function() {
|
||||||
|
// Refresh entire table!
|
||||||
|
table.bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
checkbox: true,
|
||||||
|
title: '{% trans 'Select' %}',
|
||||||
|
searchable: false,
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
title: 'ID',
|
||||||
|
visible: false,
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'part',
|
||||||
|
title: '{% trans "Part" %}',
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
|
var url = `/part/${row.sub_part}/`;
|
||||||
|
var thumb = row.sub_part_detail.thumbnail;
|
||||||
|
var name = row.sub_part_detail.full_name;
|
||||||
|
|
||||||
|
html = imageHoverIcon(thumb) + renderLink(name, url);
|
||||||
|
|
||||||
|
if (row.not_in_bom) {
|
||||||
|
html = `<i>${html}</i>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'installed',
|
||||||
|
title: '{% trans "Installed" %}',
|
||||||
|
sortable: false,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
// Construct a progress showing how many items have been installed
|
||||||
|
|
||||||
|
var installed = row.installed_count || 0;
|
||||||
|
var required = row.quantity || 0;
|
||||||
|
|
||||||
|
required *= options.quantity;
|
||||||
|
|
||||||
|
var progress = makeProgressBar(installed, required, {
|
||||||
|
id: row.sub_part.pk,
|
||||||
|
});
|
||||||
|
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
switchable: false,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var pk = row.sub_part;
|
||||||
|
|
||||||
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
|
html += makeIconButton('fa-link', 'button-install', pk, '{% trans "Install item" %}');
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
onLoadSuccess: function() {
|
||||||
|
// Grab a list of parts which are actually installed in this stock item
|
||||||
|
|
||||||
|
inventreeGet(
|
||||||
|
"{% url 'api-stock-list' %}",
|
||||||
|
{
|
||||||
|
installed_in: options.stock_item,
|
||||||
|
part_detail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
success: function(stock_items) {
|
||||||
|
|
||||||
|
var table_data = table.bootstrapTable('getData');
|
||||||
|
|
||||||
|
stock_items.forEach(function(item) {
|
||||||
|
|
||||||
|
var match = false;
|
||||||
|
|
||||||
|
for (var idx = 0; idx < table_data.length; idx++) {
|
||||||
|
|
||||||
|
var row = table_data[idx];
|
||||||
|
|
||||||
|
// Check each row in the table to see if this stock item matches
|
||||||
|
table_data.forEach(function(row) {
|
||||||
|
|
||||||
|
// Match on "sub_part"
|
||||||
|
if (row.sub_part == item.part) {
|
||||||
|
|
||||||
|
// First time?
|
||||||
|
if (row.installed_count == null) {
|
||||||
|
row.installed_count = 0;
|
||||||
|
row.installed_items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
row.installed_count += item.quantity;
|
||||||
|
row.installed_items.push(item);
|
||||||
|
|
||||||
|
// Push the row back into the table
|
||||||
|
table.bootstrapTable('updateRow', idx, row, true);
|
||||||
|
|
||||||
|
match = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
// The stock item did *not* match any items in the BOM!
|
||||||
|
// Add a new row to the table...
|
||||||
|
|
||||||
|
// Contruct a new "row" to add to the table
|
||||||
|
var new_row = {
|
||||||
|
sub_part: item.part,
|
||||||
|
sub_part_detail: item.part_detail,
|
||||||
|
not_in_bom: true,
|
||||||
|
installed_count: item.quantity,
|
||||||
|
installed_items: [item],
|
||||||
|
};
|
||||||
|
|
||||||
|
table.bootstrapTable('append', [new_row]);
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update button callback links
|
||||||
|
updateCallbacks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
updateCallbacks();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user