diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js
index 45565f1d6a..263e28fc01 100644
--- a/InvenTree/InvenTree/static/script/inventree/inventree.js
+++ b/InvenTree/InvenTree/static/script/inventree/inventree.js
@@ -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 `
-
${value} / ${maximum}
+
${text}
`;
}
diff --git a/InvenTree/InvenTree/static/script/inventree/tables.js b/InvenTree/InvenTree/static/script/inventree/tables.js
index cc4320307b..6d57240979 100644
--- a/InvenTree/InvenTree/static/script/inventree/tables.js
+++ b/InvenTree/InvenTree/static/script/inventree/tables.js
@@ -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) {
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index c70ef5c21a..9a86bb98d5 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -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 = [
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index a5c689a605..548a03ae90 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -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.
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index df1a628f47..b78b0e9410 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -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
diff --git a/InvenTree/stock/templates/stock/item_install.html b/InvenTree/stock/templates/stock/item_install.html
new file mode 100644
index 0000000000..04798972d2
--- /dev/null
+++ b/InvenTree/stock/templates/stock/item_install.html
@@ -0,0 +1,17 @@
+{% extends "modal_form.html" %}
+{% load i18n %}
+
+{% block pre_form_content %}
+
+
+ {% trans "Install another StockItem into this item." %}
+
+
+ {% trans "Stock items can only be installed if they meet the following criteria" %}:
+
+
+ - {% trans "The StockItem links to a Part which is in the BOM for this StockItem" %}
+ - {% trans "The StockItem is currently in stock" %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/stock/templates/stock/item_installed.html b/InvenTree/stock/templates/stock/item_installed.html
index cac55c9dce..2a6a0db057 100644
--- a/InvenTree/stock/templates/stock/item_installed.html
+++ b/InvenTree/stock/templates/stock/item_installed.html
@@ -10,19 +10,7 @@
{% trans "Installed Stock Items" %}
-
-
-
+
{% 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 = ``;
-
- html += makeIconButton('fa-unlink', 'button-uninstall', pk, '{% trans "Uninstall item" %}');
-
- html += `
`;
-
- 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() {
diff --git a/InvenTree/stock/templates/stock/item_serialize.html b/InvenTree/stock/templates/stock/item_serialize.html
index bb0054cca2..0f70647e38 100644
--- a/InvenTree/stock/templates/stock/item_serialize.html
+++ b/InvenTree/stock/templates/stock/item_serialize.html
@@ -1,6 +1,8 @@
{% extends "modal_form.html" %}
+{% load i18n %}
{% block pre_form_content %}
-Create serialized items from this stock item.
-Select quantity to serialize, and unique serial numbers.
+{% trans "Create serialized items from this stock item." %}
+
+{% trans "Select quantity to serialize, and unique serial numbers." %}
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py
index 4c86995cda..7ad8bc4f7f 100644
--- a/InvenTree/stock/urls.py
+++ b/InvenTree/stock/urls.py
@@ -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'),
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index c09c328c66..9d078bf702 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -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,
diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.html
index dd99c5b611..eb5e4adbf7 100644
--- a/InvenTree/templates/js/stock.html
+++ b/InvenTree/templates/js/stock.html
@@ -470,10 +470,16 @@ function loadStockTable(table, options) {
if (row.customer) {
html += ``;
- } else if (row.build_order) {
- html += ``;
- } else if (row.sales_order) {
- html += ``;
+ } else {
+ if (row.build_order) {
+ html += ``;
+ } else if (row.sales_order) {
+ html += ``;
+ }
+ }
+
+ if (row.belongs_to) {
+ html += ``;
}
// 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 = ``;
+
+ 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 += ``;
+
+ html += makeIconButton('fa-unlink', 'button-uninstall', pk, "{% trans "Uninstall stock item" %}");
+
+ html += `
`;
+
+ 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 = `${html}`
+ }
+
+ 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 = ``;
+
+ html += makeIconButton('fa-link', 'button-install', pk, '{% trans "Install item" %}');
+
+ html += `
`;
+
+ 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();
+ },
+ }
+ );
}
\ No newline at end of file