mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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 || {};
|
||||
|
||||
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) {
|
||||
percent = 100;
|
||||
@ -115,18 +120,28 @@ function makeProgressBar(value, maximum, opts) {
|
||||
|
||||
var extraclass = '';
|
||||
|
||||
if (value > maximum) {
|
||||
if (maximum) {
|
||||
// TODO - Special color?
|
||||
}
|
||||
else if (value > maximum) {
|
||||
extraclass='progress-bar-over';
|
||||
} else if (value < maximum) {
|
||||
extraclass = 'progress-bar-under';
|
||||
}
|
||||
|
||||
var text = value;
|
||||
|
||||
if (maximum) {
|
||||
text += ' / ';
|
||||
text += maximum;
|
||||
}
|
||||
|
||||
var id = options.id || 'progress-bar';
|
||||
|
||||
return `
|
||||
<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-value'>${value} / ${maximum}</div>
|
||||
<div class='progress-value'>${text}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@ -109,10 +109,20 @@ $.fn.inventreeTable = function(options) {
|
||||
options.pagination = true;
|
||||
options.pageSize = inventreeLoad(varName, 25);
|
||||
options.pageList = [25, 50, 100, 250, 'all'];
|
||||
|
||||
options.rememberOrder = true;
|
||||
options.sortable = true;
|
||||
options.search = true;
|
||||
options.showColumns = true;
|
||||
|
||||
if (options.sortable == null) {
|
||||
options.sortable = true;
|
||||
}
|
||||
|
||||
if (options.search == null) {
|
||||
options.search = true;
|
||||
}
|
||||
|
||||
if (options.showColumns == null) {
|
||||
options.showColumns = true;
|
||||
}
|
||||
|
||||
// Callback to save pagination data
|
||||
options.onPageChange = function(number, size) {
|
||||
|
@ -777,6 +777,13 @@ class BomList(generics.ListCreateAPIView):
|
||||
if sub_part is not None:
|
||||
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
|
||||
|
||||
permission_classes = [
|
||||
|
@ -8,6 +8,8 @@ from __future__ import unicode_literals
|
||||
from django import forms
|
||||
from django.forms.utils import ErrorDict
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from mptt.fields import TreeNodeChoiceField
|
||||
|
||||
@ -17,6 +19,8 @@ from InvenTree.fields import RoundingDecimalFormField
|
||||
|
||||
from report.models import TestReport
|
||||
|
||||
from part.models import Part
|
||||
|
||||
from .models import StockLocation, StockItem, StockItemTracking
|
||||
from .models import StockItemAttachment
|
||||
from .models import StockItemTestResult
|
||||
@ -271,6 +275,59 @@ class ExportOptionsForm(HelperForm):
|
||||
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):
|
||||
"""
|
||||
Form for uninstalling a stock item which is installed in another item.
|
||||
|
@ -600,12 +600,13 @@ class StockItem(MPTTModel):
|
||||
return self.installedItemCount() > 0
|
||||
|
||||
@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
|
||||
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
|
||||
notes: Any notes associated with the operation
|
||||
"""
|
||||
@ -614,18 +615,29 @@ class StockItem(MPTTModel):
|
||||
if self.belongs_to is not None:
|
||||
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
|
||||
self.belongs_to = otherItem
|
||||
|
||||
self.save()
|
||||
if stock_item is None:
|
||||
stock_item = otherItem
|
||||
|
||||
# Add a transaction note!
|
||||
self.addTransactionNote(
|
||||
_('Installed in stock item') + ' ' + str(otherItem.pk),
|
||||
# Assign the other stock item into this one
|
||||
stock_item.belongs_to = self
|
||||
stock_item.save()
|
||||
|
||||
# Add a transaction note to the other item
|
||||
stock_item.addTransactionNote(
|
||||
_('Installed into stock item') + ' ' + str(self.pk),
|
||||
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
|
||||
@ -645,16 +657,31 @@ class StockItem(MPTTModel):
|
||||
|
||||
# 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.location = location
|
||||
|
||||
self.save()
|
||||
|
||||
if location:
|
||||
url = location.get_absolute_url()
|
||||
else:
|
||||
url = ''
|
||||
|
||||
# Add a transaction note!
|
||||
self.addTransactionNote(
|
||||
_('Uninstalled into location') + ' ' + str(location),
|
||||
user,
|
||||
notes=notes
|
||||
notes=notes,
|
||||
url=url
|
||||
)
|
||||
|
||||
@property
|
||||
@ -838,20 +865,20 @@ class StockItem(MPTTModel):
|
||||
|
||||
# Do not split a serialized part
|
||||
if self.serialized:
|
||||
return
|
||||
return self
|
||||
|
||||
try:
|
||||
quantity = Decimal(quantity)
|
||||
except (InvalidOperation, ValueError):
|
||||
return
|
||||
return self
|
||||
|
||||
# Doesn't make sense for a zero quantity
|
||||
if quantity <= 0:
|
||||
return
|
||||
return self
|
||||
|
||||
# Also doesn't make sense to split the full amount
|
||||
if quantity >= self.quantity:
|
||||
return
|
||||
return self
|
||||
|
||||
# Create a new StockItem object, duplicating relevant fields
|
||||
# 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>
|
||||
<hr>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<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>
|
||||
<table class='table table-striped table-condensed' id='installed-table'></table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -30,135 +18,14 @@
|
||||
|
||||
{{ block.super }}
|
||||
|
||||
$('#installed-table').inventreeTable({
|
||||
formatNoMatches: function() {
|
||||
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,
|
||||
title: '{% trans 'Select' %}',
|
||||
searchable: false,
|
||||
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',
|
||||
]
|
||||
});
|
||||
loadInstalledInTable(
|
||||
$('#installed-table'),
|
||||
{
|
||||
stock_item: {{ item.pk }},
|
||||
part: {{ item.part.pk }},
|
||||
quantity: {{ item.quantity }},
|
||||
}
|
||||
);
|
||||
|
||||
$('#multi-item-uninstall').click(function() {
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
{% extends "modal_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
Create serialized items from this stock item.<br>
|
||||
Select quantity to serialize, and unique serial numbers.
|
||||
{% trans "Create serialized items from this stock item." %}
|
||||
<br>
|
||||
{% trans "Select quantity to serialize, and unique serial numbers." %}
|
||||
{% 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'^assign/', views.StockItemAssignToCustomer.as_view(), name='stock-item-assign'),
|
||||
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'),
|
||||
|
||||
|
@ -683,6 +683,106 @@ 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_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):
|
||||
"""
|
||||
View for uninstalling one or more StockItems,
|
||||
|
@ -470,10 +470,16 @@ function loadStockTable(table, options) {
|
||||
|
||||
if (row.customer) {
|
||||
html += `<span class='fas fa-user-tie label-right' title='{% trans "Stock item has been assigned to customer" %}'></span>`;
|
||||
} else if (row.build_order) {
|
||||
html += `<span class='fas fa-tools label-right' title='{% trans "Stock item was assigned to a build order" %}'></span>`;
|
||||
} 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>`;
|
||||
} else {
|
||||
if (row.build_order) {
|
||||
html += `<span class='fas fa-tools label-right' title='{% trans "Stock item was assigned to a build order" %}'></span>`;
|
||||
} 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>`;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@ -520,6 +526,9 @@ function loadStockTable(table, options) {
|
||||
} else if (row.customer) {
|
||||
var text = "{% trans "Shipped to customer" %}";
|
||||
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) {
|
||||
return renderLink(value, `/stock/location/${row.location}/`);
|
||||
@ -798,4 +807,301 @@ function createNewStockItem(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…
Reference in New Issue
Block a user