Further improvements to build allocation form

- Auto-allocation button ignores outputs which are complete
- StockItem API allows filtering by BomItem
- Quantity inputs are now auto-filled
- Display progress bar in the modal form
This commit is contained in:
Oliver 2021-10-05 08:25:10 +11:00
parent 0c04bfaa85
commit ae0efe73d1
9 changed files with 117 additions and 54 deletions

View File

@ -10,11 +10,15 @@ import common.models
INVENTREE_SW_VERSION = "0.6.0 dev"
INVENTREE_API_VERSION = 12
INVENTREE_API_VERSION = 13
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v13 -> 2021-10-05
- Adds API endpoint to allocate stock items against a BuildOrder
- Updates StockItem API with improved filtering against BomItem data
v12 -> 2021-09-07
- Adds API endpoint to receive stock items against a PurchaseOrder

View File

@ -9,8 +9,9 @@ import decimal
import os
from datetime import datetime
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.urls import reverse
@ -1055,8 +1056,10 @@ class BuildItem(models.Model):
Attributes:
build: Link to a Build object
bom_item: Link to a BomItem object (may or may not point to the same part as the build)
stock_item: Link to a StockItem object
quantity: Number of units allocated
install_into: Destination stock item (or None)
"""
@staticmethod

View File

@ -433,16 +433,32 @@ $("#btn-auto-allocate").on('click', function() {
var bom_items = $("#allocation-table-untracked").bootstrapTable("getData");
var incomplete_bom_items = [];
bom_items.forEach(function(bom_item) {
if (bom_item.required > bom_item.allocated) {
incomplete_bom_items.push(bom_item);
}
});
if (incomplete_bom_items.length == 0) {
showAlertDialog(
'{% trans "Allocation Complete" %}',
'{% trans "All untracked stock items have been allocated" %}',
);
} else {
allocateStockToBuild(
{{ build.pk }},
{{ build.part.pk }},
bom_items,
incomplete_bom_items,
{
success: function(data) {
$('#allocation-table-untracked').bootstrapTable('refresh');
}
}
);
}
});
$('#btn-unallocate').on('click', function() {

View File

@ -1,18 +1,6 @@
from __future__ import unicode_literals
import os
import logging
from PIL import UnidentifiedImageError
from django.apps import AppConfig
from django.db.utils import OperationalError, ProgrammingError
from django.conf import settings
from InvenTree.ready import canAppAccessDatabase
logger = logging.getLogger("inventree")
class CompanyConfig(AppConfig):

View File

@ -1,13 +1,9 @@
from __future__ import unicode_literals
import os
import logging
from django.db.utils import OperationalError, ProgrammingError
from django.apps import AppConfig
from django.conf import settings
from PIL import UnidentifiedImageError
from InvenTree.ready import canAppAccessDatabase
@ -40,7 +36,7 @@ class PartConfig(AppConfig):
items = BomItem.objects.filter(part__trackable=False, sub_part__trackable=True)
for item in items:
print(f"Marking part '{item.part.name}' as trackable")
logger.info(f"Marking part '{item.part.name}' as trackable")
item.part.trackable = True
item.part.clean()
item.part.save()

View File

@ -2329,6 +2329,23 @@ class BomItem(models.Model):
def get_api_url():
return reverse('api-bom-list')
def get_stock_filter(self):
"""
Return a queryset filter for selecting StockItems which match this BomItem
- If allow_variants is True, allow all part variants
"""
# Target part
part = self.sub_part
if self.allow_variants:
variants = part.get_descendants(include_self=True)
return Q(part__in=[v.pk for v in variants])
else:
return Q(part=part)
def save(self, *args, **kwargs):
self.clean()

View File

@ -2,11 +2,18 @@
JSON API for the Stock app
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from decimal import Decimal, InvalidOperation
from datetime import datetime, timedelta
from django.utils.translation import ugettext_lazy as _
from django.conf.urls import url, include
from django.urls import reverse
from django.http import JsonResponse
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from rest_framework import status
from rest_framework.serializers import ValidationError
@ -22,7 +29,7 @@ from .models import StockItemTracking
from .models import StockItemAttachment
from .models import StockItemTestResult
from part.models import Part, PartCategory
from part.models import BomItem, Part, PartCategory
from part.serializers import PartBriefSerializer
from company.models import Company, SupplierPart
@ -45,10 +52,6 @@ from InvenTree.helpers import str2bool, isNull
from InvenTree.api import AttachmentMixin
from InvenTree.filters import InvenTreeOrderingFilter
from decimal import Decimal, InvalidOperation
from datetime import datetime, timedelta
class StockCategoryTree(TreeSerializer):
title = _('Stock')
@ -670,14 +673,14 @@ class StockList(generics.ListCreateAPIView):
return queryset
def filter_queryset(self, queryset):
"""
Custom filtering for the StockItem queryset
"""
params = self.request.query_params
queryset = super().filter_queryset(queryset)
# Perform basic filtering:
# Note: We do not let DRF filter here, it be slow AF
supplier_part = params.get('supplier_part', None)
if supplier_part:
@ -843,6 +846,18 @@ class StockList(generics.ListCreateAPIView):
except (ValueError, PartCategory.DoesNotExist):
raise ValidationError({"category": "Invalid category id specified"})
# Does the client wish to filter by BomItem
bom_item_id = params.get('bom_item', None)
if bom_item_id is not None:
try:
bom_item = BomItem.objects.get(pk=bom_item_id)
queryset = queryset.filter(bom_item.get_stock_filter())
except (ValueError, BomItem.DoesNotExist):
pass
# Filter by StockItem status
status = params.get('status', None)

View File

@ -345,18 +345,26 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
function requiredQuantity(row) {
// Return the requied quantity for a given row
var quantity = 0;
if (output) {
// "Tracked" parts are calculated against individual build outputs
return row.quantity * output.quantity;
quantity = row.quantity * output.quantity;
} else {
// "Untracked" parts are specified against the build itself
return row.quantity * buildInfo.quantity;
quantity = row.quantity * buildInfo.quantity;
}
// Store the required quantity in the row data
row.required = quantity;
return quantity;
}
function sumAllocations(row) {
// Calculat total allocations for a given row
if (!row.allocations) {
row.allocated = 0;
return 0;
}
@ -366,6 +374,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
quantity += item.quantity;
});
row.allocated = quantity;
return quantity;
}
@ -394,7 +404,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
],
{
success: function(data) {
// TODO: Reload table
$(table).bootstrapTable('refresh');
},
}
);
@ -854,6 +864,11 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
}
);
var allocated_display = makeProgressBar(
bom_item.allocated,
bom_item.required,
);
var stock_input = constructField(
`items_stock_item_${pk}`,
{
@ -872,11 +887,12 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
<td id='part_${pk}'>
${thumb} ${sub_part.full_name}
</td>
<td id='allocated_${pk}'>
${allocated_display}
</td>
<td id='stock_item_${pk}'>
${stock_input}
</td>
<td id='allocated_${pk}'>
</td>
<td id='quantity_${pk}'>
${quantity_input}
</td>
@ -894,7 +910,15 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
for (var idx = 0; idx < bom_items.length; idx++) {
var bom_item = bom_items[idx];
table_entries += renderBomItemRow(bom_item);
var required = bom_item.required || 0;
var allocated = bom_item.allocated || 0;
var remaining = required - allocated;
if (remaining < 0) {
remaining = 0;
}
table_entries += renderBomItemRow(bom_item, remaining);
}
if (bom_items.length == 0) {
@ -913,8 +937,8 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
<thead>
<tr>
<th>{% trans "Part" %}</th>
<th style='min-width: 250px;'>{% trans "Stock Item" %}</th>
<th>{% trans "Allocated" %}</th>
<th style='min-width: 250px;'>{% trans "Stock Item" %}</th>
<th>{% trans "Quantity" %}</th>
<th></th>
</tr>
@ -942,7 +966,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
name: `items_stock_item_${bom_item.pk}`,
api_url: '{% url "api-stock-list" %}',
filters: {
part: bom_item.sub_part,
bom_item: bom_item.pk,
in_stock: true,
part_detail: false,
location_detail: true,
@ -968,7 +992,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
$(options.modal).find(`#allocation_row_${pk}`).remove();
});
},
onSubmit: function(fields, options) {
onSubmit: function(fields, opts) {
// Extract elements from the form
var data = {
@ -983,7 +1007,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
`items_quantity_${item.pk}`,
{},
{
modal: options.modal,
modal: opts.modal,
},
);
@ -991,7 +1015,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
`items_stock_item_${item.pk}`,
{},
{
modal: options.modal,
modal: opts.modal,
}
);
@ -1007,18 +1031,18 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
});
// Provide nested values
options.nested = {
opts.nested = {
"items": item_pk_values
};
inventreePut(
options.url,
opts.url,
data,
{
method: 'POST',
success: function(response) {
// Hide the modal
$(options.modal).modal('hide');
$(opts.modal).modal('hide');
if (options.success) {
options.success(response);
@ -1027,10 +1051,10 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
error: function(xhr) {
switch (xhr.status) {
case 400:
handleFormErrors(xhr.responseJSON, fields, options);
handleFormErrors(xhr.responseJSON, fields, opts);
break;
default:
$(options.modal).modal('hide');
$(opts.modal).modal('hide');
showApiError(xhr);
break;
}