Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver 2021-06-02 18:53:52 +10:00
commit 1f6fc5eb1a
15 changed files with 339 additions and 292 deletions

View File

@ -16,9 +16,13 @@ Increment thi API version number whenever there is a significant change to the A
v3 -> 2021-05-22: v3 -> 2021-05-22:
- The updated StockItem "history tracking" now uses a different interface - The updated StockItem "history tracking" now uses a different interface
v4 -> 2021-06-01
- BOM items can now accept "variant stock" to be assigned against them
- Many slight API tweaks were needed to get this to work properly!
""" """
INVENTREE_API_VERSION = 3 INVENTREE_API_VERSION = 4
def inventreeInstanceName(): def inventreeInstanceName():

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2 on 2021-06-01 05:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0066_bomitem_allow_variants'),
('build', '0027_auto_20210404_2016'),
]
operations = [
migrations.AddField(
model_name='builditem',
name='bom_item',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='allocate_build_items', to='part.bomitem'),
),
]

View File

@ -0,0 +1,62 @@
# Generated by Django 3.2 on 2021-06-01 05:25
from django.db import migrations
def assign_bom_items(apps, schema_editor):
"""
Run through existing BuildItem objects,
and assign a matching BomItem
"""
BuildItem = apps.get_model('build', 'builditem')
BomItem = apps.get_model('part', 'bomitem')
Part = apps.get_model('part', 'part')
print("Assigning BomItems to existing BuildItem objects")
count_valid = 0
count_total = 0
for build_item in BuildItem.objects.all():
# Try to find a BomItem which matches the BuildItem
# Note: Before this migration, variant stock assignment was not allowed,
# so BomItem lookup should be pretty easy
count_total += 1
try:
bom_item = BomItem.objects.get(
part__id=build_item.build.part.pk,
sub_part__id=build_item.stock_item.part.pk,
)
build_item.bom_item = bom_item
build_item.save()
count_valid += 1
except BomItem.DoesNotExist:
pass
print(f"Assigned BomItem for {count_valid}/{count_total} entries")
def unassign_bom_items(apps, schema_editor):
"""
Reverse migration does not do anything.
Function here to preserve ability to reverse migration
"""
pass
class Migration(migrations.Migration):
dependencies = [
('build', '0028_builditem_bom_item'),
]
operations = [
migrations.RunPython(assign_bom_items, reverse_code=unassign_bom_items),
]

View File

@ -30,6 +30,7 @@ from InvenTree.models import InvenTreeAttachment
import common.models import common.models
import InvenTree.fields import InvenTree.fields
import InvenTree.helpers
from stock import models as StockModels from stock import models as StockModels
from part import models as PartModels from part import models as PartModels
@ -880,9 +881,12 @@ class Build(MPTTModel):
output - Build output (StockItem). output - Build output (StockItem).
""" """
# Remember, if 'variant' stock is allowed to be allocated, it becomes more complicated!
variants = part.get_descendants(include_self=True)
allocations = BuildItem.objects.filter( allocations = BuildItem.objects.filter(
build=self, build=self,
stock_item__part=part, stock_item__part__pk__in=[p.pk for p in variants],
install_into=output, install_into=output,
) )
@ -1036,6 +1040,18 @@ class Build(MPTTModel):
StockModels.StockItem.IN_STOCK_FILTER StockModels.StockItem.IN_STOCK_FILTER
) )
# Check if variants are allowed for this part
try:
bom_item = PartModels.BomItem.objects.get(part=self.part, sub_part=part)
allow_part_variants = bom_item.allow_variants
except PartModels.BomItem.DoesNotExist:
allow_part_variants = False
if allow_part_variants:
parts = part.get_descendants(include_self=True)
items = items.filter(part__pk__in=[p.pk for p in parts])
else:
items = items.filter(part=part) items = items.filter(part=part)
# Exclude any items which have already been allocated # Exclude any items which have already been allocated
@ -1160,10 +1176,6 @@ class BuildItem(models.Model):
if self.stock_item.part and self.stock_item.part.trackable and not self.install_into: if self.stock_item.part and self.stock_item.part.trackable and not self.install_into:
raise ValidationError(_('Build item must specify a build output, as master part is marked as trackable')) raise ValidationError(_('Build item must specify a build output, as master part is marked as trackable'))
# Allocated part must be in the BOM for the master part
if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False):
errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)]
# Allocated quantity cannot exceed available stock quantity # Allocated quantity cannot exceed available stock quantity
if self.quantity > self.stock_item.quantity: if self.quantity > self.stock_item.quantity:
errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})").format( errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})").format(
@ -1189,6 +1201,61 @@ class BuildItem(models.Model):
if len(errors) > 0: if len(errors) > 0:
raise ValidationError(errors) raise ValidationError(errors)
"""
Attempt to find the "BomItem" which links this BuildItem to the build.
- If a BomItem is already set, and it is valid, then we are ok!
"""
bom_item_valid = False
if self.bom_item:
"""
A BomItem object has already been assigned. This is valid if:
a) It points to the same "part" as the referened build
b) Either:
i) The sub_part points to the same part as the referenced StockItem
ii) The BomItem allows variants and the part referenced by the StockItem
is a variant of the sub_part referenced by the BomItem
"""
if self.build and self.build.part == self.bom_item.part:
# Check that the sub_part points to the stock_item (either directly or via a variant)
if self.bom_item.sub_part == self.stock_item.part:
bom_item_valid = True
elif self.bom_item.allow_variants and self.stock_item.part in self.bom_item.sub_part.get_descendants(include_self=False):
bom_item_valid = True
# If the existing BomItem is *not* valid, try to find a match
if not bom_item_valid:
if self.build and self.stock_item:
ancestors = self.stock_item.part.get_ancestors(include_self=True, ascending=True)
for idx, ancestor in enumerate(ancestors):
try:
bom_item = PartModels.BomItem.objects.get(part=self.build.part, sub_part=ancestor)
except PartModels.BomItem.DoesNotExist:
continue
# A matching BOM item has been found!
if idx == 0 or bom_item.allow_variants:
bom_item_valid = True
self.bom_item = bom_item
break
# BomItem did not exist or could not be validated.
# Search for a new one
if not bom_item_valid:
raise ValidationError({
'stock_item': _("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)
})
@transaction.atomic @transaction.atomic
def complete_allocation(self, user): def complete_allocation(self, user):
""" """
@ -1217,6 +1284,18 @@ class BuildItem(models.Model):
# Simply remove the items from stock # Simply remove the items from stock
item.take_stock(self.quantity, user) item.take_stock(self.quantity, user)
def getStockItemThumbnail(self):
"""
Return qualified URL for part thumbnail image
"""
if self.stock_item and self.stock_item.part:
return InvenTree.helpers.getMediaUrl(self.stock_item.part.image.thumbnail.url)
elif self.bom_item and self.stock_item.sub_part:
return InvenTree.helpers.getMediaUrl(self.bom_item.sub_part.image.thumbnail.url)
else:
return InvenTree.helpers.getBlankThumbnail()
build = models.ForeignKey( build = models.ForeignKey(
Build, Build,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -1225,6 +1304,15 @@ class BuildItem(models.Model):
help_text=_('Build to allocate parts') help_text=_('Build to allocate parts')
) )
# Internal model which links part <-> sub_part
# We need to track this separately, to allow for "variant' stock
bom_item = models.ForeignKey(
PartModels.BomItem,
on_delete=models.CASCADE,
related_name='allocate_build_items',
blank=True, null=True,
)
stock_item = models.ForeignKey( stock_item = models.ForeignKey(
'stock.StockItem', 'stock.StockItem',
on_delete=models.CASCADE, on_delete=models.CASCADE,

View File

@ -97,9 +97,10 @@ class BuildSerializer(InvenTreeModelSerializer):
class BuildItemSerializer(InvenTreeModelSerializer): class BuildItemSerializer(InvenTreeModelSerializer):
""" Serializes a BuildItem object """ """ Serializes a BuildItem object """
bom_part = serializers.IntegerField(source='bom_item.sub_part.pk', read_only=True)
part = serializers.IntegerField(source='stock_item.part.pk', read_only=True) part = serializers.IntegerField(source='stock_item.part.pk', read_only=True)
part_name = serializers.CharField(source='stock_item.part.full_name', read_only=True) part_name = serializers.CharField(source='stock_item.part.full_name', read_only=True)
part_image = serializers.CharField(source='stock_item.part.image', read_only=True) part_thumb = serializers.CharField(source='getStockItemThumbnail', read_only=True)
stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True) stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
quantity = serializers.FloatField() quantity = serializers.FloatField()
@ -108,11 +109,12 @@ class BuildItemSerializer(InvenTreeModelSerializer):
model = BuildItem model = BuildItem
fields = [ fields = [
'pk', 'pk',
'bom_part',
'build', 'build',
'install_into', 'install_into',
'part', 'part',
'part_name', 'part_name',
'part_image', 'part_thumb',
'stock_item', 'stock_item',
'stock_item_detail', 'stock_item_detail',
'quantity' 'quantity'

View File

@ -821,6 +821,14 @@ class BomList(generics.ListCreateAPIView):
queryset = queryset.filter(inherited=inherited) queryset = queryset.filter(inherited=inherited)
# Filter by "allow_variants"
variants = params.get("allow_variants", None)
if variants is not None:
variants = str2bool(variants)
queryset = queryset.filter(allow_variants=variants)
# Filter by part? # Filter by part?
part = params.get('part', None) part = params.get('part', None)

View File

@ -352,6 +352,7 @@ class EditBomItemForm(HelperForm):
'reference', 'reference',
'overage', 'overage',
'note', 'note',
'allow_variants',
'inherited', 'inherited',
'optional', 'optional',
] ]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2021-06-01 03:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0065_auto_20210505_2144'),
]
operations = [
migrations.AddField(
model_name='bomitem',
name='allow_variants',
field=models.BooleanField(default=False, help_text='Stock items for variant parts can be used for this BOM item', verbose_name='Allow Variants'),
),
]

View File

@ -2240,6 +2240,7 @@ class BomItem(models.Model):
note: Note field for this BOM item note: Note field for this BOM item
checksum: Validation checksum for the particular BOM line item checksum: Validation checksum for the particular BOM line item
inherited: This BomItem can be inherited by the BOMs of variant parts inherited: This BomItem can be inherited by the BOMs of variant parts
allow_variants: Stock for part variants can be substituted for this BomItem
""" """
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -2288,6 +2289,12 @@ class BomItem(models.Model):
help_text=_('This BOM item is inherited by BOMs for variant parts'), help_text=_('This BOM item is inherited by BOMs for variant parts'),
) )
allow_variants = models.BooleanField(
default=False,
verbose_name=_('Allow Variants'),
help_text=_('Stock items for variant parts can be used for this BOM item')
)
def get_item_hash(self): def get_item_hash(self):
""" Calculate the checksum hash of this BOM line item: """ Calculate the checksum hash of this BOM line item:

View File

@ -453,6 +453,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
class Meta: class Meta:
model = BomItem model = BomItem
fields = [ fields = [
'allow_variants',
'inherited', 'inherited',
'note', 'note',
'optional', 'optional',
@ -460,16 +461,16 @@ class BomItemSerializer(InvenTreeModelSerializer):
'pk', 'pk',
'part', 'part',
'part_detail', 'part_detail',
'purchase_price_avg',
'purchase_price_max',
'purchase_price_min',
'purchase_price_range',
'quantity', 'quantity',
'reference', 'reference',
'sub_part', 'sub_part',
'sub_part_detail', 'sub_part_detail',
# 'price_range', # 'price_range',
'validated', 'validated',
'purchase_price_min',
'purchase_price_max',
'purchase_price_avg',
'purchase_price_range',
] ]

View File

@ -285,11 +285,18 @@ function loadBomTable(table, options) {
title: '{% trans "Optional" %}', title: '{% trans "Optional" %}',
searchable: false, searchable: false,
formatter: function(value) { formatter: function(value) {
if (value == '1') return '{% trans "true" %}'; return yesNoLabel(value);
if (value == '0') return '{% trans "false" %}';
} }
}); });
cols.push({
field: 'allow_variants',
title: '{% trans "Allow Variants" %}',
formatter: function(value) {
return yesNoLabel(value);
}
})
cols.push({ cols.push({
field: 'inherited', field: 'inherited',
title: '{% trans "Inherited" %}', title: '{% trans "Inherited" %}',
@ -297,7 +304,7 @@ function loadBomTable(table, options) {
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
// This BOM item *is* inheritable, but is defined for this BOM // This BOM item *is* inheritable, but is defined for this BOM
if (!row.inherited) { if (!row.inherited) {
return "-"; return yesNoLabel(false);
} else if (row.part == options.parent_id) { } else if (row.part == options.parent_id) {
return '{% trans "Inherited" %}'; return '{% trans "Inherited" %}';
} else { } else {

View File

@ -372,7 +372,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
data.forEach(function(item) { data.forEach(function(item) {
// Group BuildItem objects by part // Group BuildItem objects by part
var part = item.part; var part = item.bom_part || item.part;
var key = parseInt(part); var key = parseInt(part);
if (!(key in allocations)) { if (!(key in allocations)) {
@ -461,6 +461,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
data: row.allocations, data: row.allocations,
showHeader: true, showHeader: true,
columns: [ columns: [
{
field: 'part',
title: '{% trans "Part" %}',
formatter: function(value, row) {
var html = imageHoverIcon(row.part_thumb);
html += renderLink(row.part_name, `/part/${value}/`);
return html;
}
},
{ {
width: '50%', width: '50%',
field: 'quantity', field: 'quantity',

View File

@ -5,6 +5,14 @@
* Requires api.js to be loaded first * Requires api.js to be loaded first
*/ */
function yesNoLabel(value) {
if (value) {
return `<span class='label label-green'>{% trans "YES" %}</span>`;
} else {
return `<span class='label label-yellow'>{% trans "NO" %}</span>`;
}
}
function toggleStar(options) { function toggleStar(options) {
/* Toggle the 'starred' status of a part. /* Toggle the 'starred' status of a part.
* Performs AJAX queries and updates the display on the button. * Performs AJAX queries and updates the display on the button.
@ -662,16 +670,6 @@ function loadPartCategoryTable(table, options) {
}); });
} }
function yesNoLabel(value) {
if (value) {
return `<span class='label label-green'>{% trans "YES" %}</span>`;
} else {
return `<span class='label label-yellow'>{% trans "NO" %}</span>`;
}
}
function loadPartTestTemplateTable(table, options) { function loadPartTestTemplateTable(table, options) {
/* /*
* Load PartTestTemplate table. * Load PartTestTemplate table.

View File

@ -81,15 +81,17 @@ function loadStockTestResultsTable(table, options) {
return html; return html;
} }
var parent_node = "parent node";
table.inventreeTable({ table.inventreeTable({
url: "{% url 'api-part-test-template-list' %}", url: "{% url 'api-part-test-template-list' %}",
method: 'get', method: 'get',
name: 'testresult', name: 'testresult',
treeEnable: true, treeEnable: true,
rootParentId: options.stock_item, rootParentId: parent_node,
parentIdField: 'parent', parentIdField: 'parent',
idField: 'pk', idField: 'pk',
uniqueId: 'pk', uniqueId: 'key',
treeShowField: 'test_name', treeShowField: 'test_name',
formatNoMatches: function() { formatNoMatches: function() {
return '{% trans "No test results found" %}'; return '{% trans "No test results found" %}';
@ -190,7 +192,7 @@ function loadStockTestResultsTable(table, options) {
match = true; match = true;
if (row.result == null) { if (row.result == null) {
item.parent = options.stock_item; item.parent = parent_node;
tableData[index] = item; tableData[index] = item;
override = true; override = true;
} else { } else {
@ -202,7 +204,7 @@ function loadStockTestResultsTable(table, options) {
// No match could be found // No match could be found
if (!match) { if (!match) {
item.test_name = item.test; item.test_name = item.test;
item.parent = options.stock_item; item.parent = parent_node;
} }
if (!override) { if (!override) {
@ -1302,33 +1304,6 @@ function createNewStockItem(options) {
function loadInstalledInTable(table, options) { function loadInstalledInTable(table, options) {
/* /*
* Display a table showing the stock items which are installed in this stock item. * 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() { function updateCallbacks() {
@ -1351,56 +1326,48 @@ function loadInstalledInTable(table, options) {
}); });
} }
table.inventreeTable( table.inventreeTable({
{ url: "{% url 'api-stock-list' %}",
url: "{% url 'api-bom-list' %}",
queryParams: { queryParams: {
part: options.part, installed_in: options.stock_item,
sub_part_trackable: true, part_detail: true,
sub_part_detail: true,
}, },
showColumns: false, formatNoMatches: function() {
name: 'installed-in', return '{% trans "No installed items" %}';
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: [ columns: [
{ {
field: 'item', field: 'part',
title: '{% trans "Stock Item" %}', title: '{% trans "Part" %}',
formatter: function(value, subrow, index, field) { formatter: function(value, row) {
var pk = subrow.pk;
var html = ''; var html = '';
if (subrow.serial && subrow.quantity == 1) { html += imageHoverIcon(row.part_detail.thumbnail);
html += `{% trans "Serial" %}: ${subrow.serial}`; html += renderLink(row.part_detail.full_name, `/stock/item/${row.pk}/`);
return html;
}
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
formatter: function(value, row) {
var html = '';
if (row.serial && row.quantity == 1) {
html += `{% trans "Serial" %}: ${row.serial}`;
} else { } else {
html += `{% trans "Quantity" %}: ${subrow.quantity}`; html += `${row.quantity}`;
} }
return renderLink(html, `/stock/item/${subrow.pk}/`); return renderLink(html, `/stock/item/${row.pk}/`);
}, }
}, },
{ {
field: 'status', field: 'status',
title: '{% trans "Status" %}', title: '{% trans "Status" %}',
formatter: function(value, subrow, index, field) { formatter: function(value, row) {
return stockStatusDisplay(value); return stockStatusDisplay(value);
} }
}, },
@ -1409,18 +1376,15 @@ function loadInstalledInTable(table, options) {
title: '{% trans "Batch" %}', title: '{% trans "Batch" %}',
}, },
{ {
field: 'actions', field: 'buttons',
title: '', title: '',
formatter: function(value, subrow, index) { switchable: false,
formatter: function(value, row) {
var pk = subrow.pk; var pk = row.pk;
var html = ''; var html = '';
// Add some buttons yo!
html += `<div class='btn-group float-right' role='group'>`; html += `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-unlink', 'button-uninstall', pk, '{% trans "Uninstall Stock Item" %}');
html += makeIconButton('fa-unlink', 'button-uninstall', pk, "{% trans 'Uninstall stock item' %}");
html += `</div>`; html += `</div>`;
return html; return html;
@ -1428,169 +1392,22 @@ function loadInstalledInTable(table, options) {
} }
], ],
onPostBody: function() { onPostBody: function() {
// Setup button callbacks // Assign callbacks to the buttons
subTable.find('.button-uninstall').click(function() { table.find('.button-uninstall').click(function() {
var pk = $(this).attr('pk'); var pk = $(this).attr('pk');
launchModalForm( launchModalForm(
"{% url 'stock-item-uninstall' %}", '{% url "stock-item-uninstall" %}',
{ {
data: { data: {
'items[]': [pk], 'items[]': pk,
}, },
success: function() { success: function() {
// Refresh entire table!
table.bootstrapTable('refresh'); 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();
},
}
);
} }

View File

@ -49,6 +49,10 @@ function getAvailableTableFilters(tableKey) {
inherited: { inherited: {
type: 'bool', type: 'bool',
title: '{% trans "Inherited" %}', title: '{% trans "Inherited" %}',
},
allow_variants: {
type: 'bool',
title: '{% trans "Allow Variant Stock" %}',
} }
}; };
} }