mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +00:00 
			
		
		
		
	Improvements to allocation of stock items against build orders
- Refactor functions for filtering stock using bom_item pk - Allow selection of substitute items when allocating against build order - Improvements for modal rendering - Don't display filter drop-down if there are no filters available
This commit is contained in:
		@@ -1153,16 +1153,12 @@ class BuildItem(models.Model):
 | 
				
			|||||||
                i) The sub_part points to the same part as the referenced StockItem
 | 
					                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
 | 
					                ii) The BomItem allows variants and the part referenced by the StockItem
 | 
				
			||||||
                    is a variant of the sub_part referenced by the BomItem
 | 
					                    is a variant of the sub_part referenced by the BomItem
 | 
				
			||||||
 | 
					                iii) The Part referenced by the StockItem is a valid substitute for the BomItem
 | 
				
			||||||
            """
 | 
					            """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if self.build and self.build.part == self.bom_item.part:
 | 
					            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)
 | 
					                bom_item_valid = self.bom_item.is_stock_item_valid(self.stock_item)
 | 
				
			||||||
                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 the existing BomItem is *not* valid, try to find a match
 | 
				
			||||||
        if not bom_item_valid:
 | 
					        if not bom_item_valid:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -197,7 +197,7 @@
 | 
				
			|||||||
                    <button id='allocate-selected-items' class='btn btn-success' title='{% trans "Allocate selected items" %}'>
 | 
					                    <button id='allocate-selected-items' class='btn btn-success' title='{% trans "Allocate selected items" %}'>
 | 
				
			||||||
                        <span class='fas fa-sign-in-alt'></span>
 | 
					                        <span class='fas fa-sign-in-alt'></span>
 | 
				
			||||||
                    </button>
 | 
					                    </button>
 | 
				
			||||||
                    <div class='filter-list' id='filter-list-build-items'>
 | 
					                    <div class='filter-list' id='filter-list-builditems'>
 | 
				
			||||||
                        <!-- Empty div for table filters-->
 | 
					                        <!-- Empty div for table filters-->
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2333,6 +2333,38 @@ class BomItem(models.Model):
 | 
				
			|||||||
    def get_api_url():
 | 
					    def get_api_url():
 | 
				
			||||||
        return reverse('api-bom-list')
 | 
					        return reverse('api-bom-list')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_valid_parts_for_allocation(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Return a list of valid parts which can be allocated against this BomItem:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        - Include the referenced sub_part
 | 
				
			||||||
 | 
					        - Include any directly specvified substitute parts
 | 
				
			||||||
 | 
					        - If allow_variants is True, allow all variants of sub_part
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Set of parts we will allow
 | 
				
			||||||
 | 
					        parts = set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        parts.add(self.sub_part)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Variant parts (if allowed)
 | 
				
			||||||
 | 
					        if self.allow_variants:
 | 
				
			||||||
 | 
					            for variant in self.sub_part.get_descendants(include_self=False):
 | 
				
			||||||
 | 
					                parts.add(variant)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Substitute parts
 | 
				
			||||||
 | 
					        for sub in self.substitutes.all():
 | 
				
			||||||
 | 
					            parts.add(sub.part)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return parts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_stock_item_valid(self, stock_item):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Check if the provided StockItem object is "valid" for assignment against this BomItem
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return stock_item.part in self.get_valid_parts_for_allocation()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_stock_filter(self):
 | 
					    def get_stock_filter(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return a queryset filter for selecting StockItems which match this BomItem
 | 
					        Return a queryset filter for selecting StockItems which match this BomItem
 | 
				
			||||||
@@ -2342,24 +2374,7 @@ class BomItem(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # List of parts we allow
 | 
					        return Q(part__in=[part.pk for part in self.get_valid_parts_for_allocation()])
 | 
				
			||||||
        part_ids = set()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        part_ids.add(self.sub_part.pk)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Variant parts (if allowed)
 | 
					 | 
				
			||||||
        if self.allow_variants:
 | 
					 | 
				
			||||||
            variants = self.sub_part.get_descendants(include_self=False)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for v in variants:
 | 
					 | 
				
			||||||
                part_ids.add(v.pk)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Direct substitute parts
 | 
					 | 
				
			||||||
        for sub in self.substitutes.all():
 | 
					 | 
				
			||||||
            part_ids.add(sub.part.pk)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Return a list of Part ID values which can be filtered against
 | 
					 | 
				
			||||||
        return Q(part__in=[pk for pk in part_ids])
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -348,6 +348,17 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
 | 
				
			|||||||
        table = `#allocation-table-${outputId}`;
 | 
					        table = `#allocation-table-${outputId}`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Filters
 | 
				
			||||||
 | 
					    var filters = loadTableFilters('builditems');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var params = options.params || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var key in params) {
 | 
				
			||||||
 | 
					        filters[key] = params[key];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setupFilterList('builditems', $(table), options.filterTarget || null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // If an "output" is specified, then only "trackable" parts are allocated
 | 
					    // If an "output" is specified, then only "trackable" parts are allocated
 | 
				
			||||||
    // Otherwise, only "untrackable" parts are allowed
 | 
					    // Otherwise, only "untrackable" parts are allowed
 | 
				
			||||||
    var trackable = ! !output;
 | 
					    var trackable = ! !output;
 | 
				
			||||||
@@ -726,6 +737,10 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    html += makePartIcons(row.sub_part_detail);
 | 
					                    html += makePartIcons(row.sub_part_detail);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (row.substitutes && row.substitutes.length > 0) {
 | 
				
			||||||
 | 
					                        html += makeIconBadge('fa-exchange-alt', '{% trans "Substitutes Available" %}');
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    return html;
 | 
					                    return html;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
@@ -1021,12 +1036,12 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
 | 
				
			|||||||
                        filters: {
 | 
					                        filters: {
 | 
				
			||||||
                            bom_item: bom_item.pk,
 | 
					                            bom_item: bom_item.pk,
 | 
				
			||||||
                            in_stock: true,
 | 
					                            in_stock: true,
 | 
				
			||||||
                            part_detail: false,
 | 
					                            part_detail: true,
 | 
				
			||||||
                            location_detail: true,
 | 
					                            location_detail: true,
 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
                        model: 'stockitem',
 | 
					                        model: 'stockitem',
 | 
				
			||||||
                        required: true,
 | 
					                        required: true,
 | 
				
			||||||
                        render_part_detail: false,
 | 
					                        render_part_detail: true,
 | 
				
			||||||
                        render_location_detail: true,
 | 
					                        render_location_detail: true,
 | 
				
			||||||
                        auto_fill: true,
 | 
					                        auto_fill: true,
 | 
				
			||||||
                        adjustFilters: function(filters) {
 | 
					                        adjustFilters: function(filters) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -283,6 +283,8 @@ function setupFilterList(tableKey, table, target) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    element.append(`<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-default filter-tag'><span class='fas fa-redo-alt'></span></button>`);
 | 
					    element.append(`<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-default filter-tag'><span class='fas fa-redo-alt'></span></button>`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If there are available filters, add them in!
 | 
				
			||||||
 | 
					    if (filters.length > 0) {
 | 
				
			||||||
        element.append(`<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-default filter-tag'><span class='fas fa-filter'></span></button>`);
 | 
					        element.append(`<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-default filter-tag'><span class='fas fa-filter'></span></button>`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (Object.keys(filters).length > 0) {
 | 
					        if (Object.keys(filters).length > 0) {
 | 
				
			||||||
@@ -296,6 +298,7 @@ function setupFilterList(tableKey, table, target) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`);
 | 
					            element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Callback for reloading the table
 | 
					    // Callback for reloading the table
 | 
				
			||||||
    element.find(`#reload-${tableKey}`).click(function() {
 | 
					    element.find(`#reload-${tableKey}`).click(function() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,31 +53,16 @@ function renderStockItem(name, data, parameters, options) {
 | 
				
			|||||||
        image = data.part_detail.thumbnail || data.part_detail.image || blankImage();
 | 
					        image = data.part_detail.thumbnail || data.part_detail.image || blankImage();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    var html = '';
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    var render_part_detail = true;
 | 
					    var render_part_detail = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if ('render_part_detail' in parameters) {
 | 
					    if ('render_part_detail' in parameters) {
 | 
				
			||||||
        render_part_detail = parameters['render_part_detail'];
 | 
					        render_part_detail = parameters['render_part_detail'];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var part_detail = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (render_part_detail) {
 | 
					    if (render_part_detail) {
 | 
				
			||||||
        html += `<img src='${image}' class='select2-thumbnail'>`;
 | 
					        part_detail = `<img src='${image}' class='select2-thumbnail'><span>${data.part_detail.full_name}</span> - `;
 | 
				
			||||||
        html += ` <span>${data.part_detail.full_name || data.part_detail.name}</span>`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    html += '<span>';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (data.serial && data.quantity == 1) {
 | 
					 | 
				
			||||||
        html += `{% trans "Serial Number" %}: ${data.serial}`;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        html += `{% trans "Quantity" %}: ${data.quantity}`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    html += '</span>';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (render_part_detail && data.part_detail.description) {
 | 
					 | 
				
			||||||
        html += `<p><small>${data.part_detail.description}</small></p>`;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var render_stock_id = true;
 | 
					    var render_stock_id = true;
 | 
				
			||||||
@@ -86,8 +71,10 @@ function renderStockItem(name, data, parameters, options) {
 | 
				
			|||||||
        render_stock_id = parameters['render_stock_id'];
 | 
					        render_stock_id = parameters['render_stock_id'];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var stock_id = '';
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    if (render_stock_id) {
 | 
					    if (render_stock_id) {
 | 
				
			||||||
        html += `<span class='float-right'><small>{% trans "Stock ID" %}: ${data.pk}</small></span>`;
 | 
					        stock_id = `<span class='float-right'><small>{% trans "Stock ID" %}: ${data.pk}</small></span>`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var render_location_detail = false;
 | 
					    var render_location_detail = false;
 | 
				
			||||||
@@ -96,10 +83,28 @@ function renderStockItem(name, data, parameters, options) {
 | 
				
			|||||||
        render_location_detail = parameters['render_location_detail'];
 | 
					        render_location_detail = parameters['render_location_detail'];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var location_detail = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (render_location_detail && data.location_detail) {
 | 
					    if (render_location_detail && data.location_detail) {
 | 
				
			||||||
        html += `<span> - ${data.location_detail.name}</span>`;
 | 
					        location_detail = ` - (<em>${data.location_detail.name}</em>)`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var stock_detail = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (data.serial && data.quantity == 1) {
 | 
				
			||||||
 | 
					        stock_detail = `{% trans "Serial Number" %}: ${data.serial}`;
 | 
				
			||||||
 | 
					    } else if (data.quantity == 0) {
 | 
				
			||||||
 | 
					        stock_detail = `<span class='label-form label-red'>{% trans "No Stock"% }</span>`;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        stock_detail = `{% trans "Quantity" %}: ${data.quantity}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var html = `
 | 
				
			||||||
 | 
					    <span>
 | 
				
			||||||
 | 
					        ${part_detail}${stock_detail}${location_detail}${stock_id}
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html;
 | 
					    return html;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -592,7 +592,7 @@ function loadPartParameterTable(table, url, options) {
 | 
				
			|||||||
        filters[key] = params[key];
 | 
					        filters[key] = params[key];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // setupFilterLsit("#part-parameters", $(table));
 | 
					    // setupFilterList("#part-parameters", $(table));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $(table).inventreeTable({
 | 
					    $(table).inventreeTable({
 | 
				
			||||||
        url: url,
 | 
					        url: url,
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user