mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
0c04bfaa85
commit
ae0efe73d1
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -433,16 +433,32 @@ $("#btn-auto-allocate").on('click', function() {
|
||||
|
||||
var bom_items = $("#allocation-table-untracked").bootstrapTable("getData");
|
||||
|
||||
allocateStockToBuild(
|
||||
{{ build.pk }},
|
||||
{{ build.part.pk }},
|
||||
bom_items,
|
||||
{
|
||||
success: function(data) {
|
||||
$('#allocation-table-untracked').bootstrapTable('refresh');
|
||||
}
|
||||
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 }},
|
||||
incomplete_bom_items,
|
||||
{
|
||||
success: function(data) {
|
||||
$('#allocation-table-untracked').bootstrapTable('refresh');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$('#btn-unallocate').on('click', function() {
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user