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_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
|
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
|
v12 -> 2021-09-07
|
||||||
- Adds API endpoint to receive stock items against a PurchaseOrder
|
- Adds API endpoint to receive stock items against a PurchaseOrder
|
||||||
|
|
||||||
|
@ -9,8 +9,9 @@ import decimal
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -1055,8 +1056,10 @@ class BuildItem(models.Model):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
build: Link to a Build object
|
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
|
stock_item: Link to a StockItem object
|
||||||
quantity: Number of units allocated
|
quantity: Number of units allocated
|
||||||
|
install_into: Destination stock item (or None)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -433,16 +433,32 @@ $("#btn-auto-allocate").on('click', function() {
|
|||||||
|
|
||||||
var bom_items = $("#allocation-table-untracked").bootstrapTable("getData");
|
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(
|
allocateStockToBuild(
|
||||||
{{ build.pk }},
|
{{ build.pk }},
|
||||||
{{ build.part.pk }},
|
{{ build.part.pk }},
|
||||||
bom_items,
|
incomplete_bom_items,
|
||||||
{
|
{
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
$('#allocation-table-untracked').bootstrapTable('refresh');
|
$('#allocation-table-untracked').bootstrapTable('refresh');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#btn-unallocate').on('click', function() {
|
$('#btn-unallocate').on('click', function() {
|
||||||
|
@ -1,18 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from PIL import UnidentifiedImageError
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
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):
|
class CompanyConfig(AppConfig):
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from PIL import UnidentifiedImageError
|
|
||||||
|
|
||||||
from InvenTree.ready import canAppAccessDatabase
|
from InvenTree.ready import canAppAccessDatabase
|
||||||
|
|
||||||
@ -40,7 +36,7 @@ class PartConfig(AppConfig):
|
|||||||
items = BomItem.objects.filter(part__trackable=False, sub_part__trackable=True)
|
items = BomItem.objects.filter(part__trackable=False, sub_part__trackable=True)
|
||||||
|
|
||||||
for item in items:
|
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.trackable = True
|
||||||
item.part.clean()
|
item.part.clean()
|
||||||
item.part.save()
|
item.part.save()
|
||||||
|
@ -2329,6 +2329,23 @@ class BomItem(models.Model):
|
|||||||
def get_api_url():
|
def get_api_url():
|
||||||
return reverse('api-bom-list')
|
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):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
self.clean()
|
self.clean()
|
||||||
|
@ -2,11 +2,18 @@
|
|||||||
JSON API for the Stock app
|
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.conf.urls import url, include
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
@ -22,7 +29,7 @@ from .models import StockItemTracking
|
|||||||
from .models import StockItemAttachment
|
from .models import StockItemAttachment
|
||||||
from .models import StockItemTestResult
|
from .models import StockItemTestResult
|
||||||
|
|
||||||
from part.models import Part, PartCategory
|
from part.models import BomItem, Part, PartCategory
|
||||||
from part.serializers import PartBriefSerializer
|
from part.serializers import PartBriefSerializer
|
||||||
|
|
||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
@ -45,10 +52,6 @@ from InvenTree.helpers import str2bool, isNull
|
|||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
|
|
||||||
from decimal import Decimal, InvalidOperation
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
|
|
||||||
class StockCategoryTree(TreeSerializer):
|
class StockCategoryTree(TreeSerializer):
|
||||||
title = _('Stock')
|
title = _('Stock')
|
||||||
@ -670,14 +673,14 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
|
"""
|
||||||
|
Custom filtering for the StockItem queryset
|
||||||
|
"""
|
||||||
|
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
|
||||||
queryset = super().filter_queryset(queryset)
|
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)
|
supplier_part = params.get('supplier_part', None)
|
||||||
|
|
||||||
if supplier_part:
|
if supplier_part:
|
||||||
@ -843,6 +846,18 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
except (ValueError, PartCategory.DoesNotExist):
|
except (ValueError, PartCategory.DoesNotExist):
|
||||||
raise ValidationError({"category": "Invalid category id specified"})
|
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
|
# Filter by StockItem status
|
||||||
status = params.get('status', None)
|
status = params.get('status', None)
|
||||||
|
|
||||||
|
@ -345,18 +345,26 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
function requiredQuantity(row) {
|
function requiredQuantity(row) {
|
||||||
// Return the requied quantity for a given row
|
// Return the requied quantity for a given row
|
||||||
|
|
||||||
|
var quantity = 0;
|
||||||
|
|
||||||
if (output) {
|
if (output) {
|
||||||
// "Tracked" parts are calculated against individual build outputs
|
// "Tracked" parts are calculated against individual build outputs
|
||||||
return row.quantity * output.quantity;
|
quantity = row.quantity * output.quantity;
|
||||||
} else {
|
} else {
|
||||||
// "Untracked" parts are specified against the build itself
|
// "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) {
|
function sumAllocations(row) {
|
||||||
// Calculat total allocations for a given row
|
// Calculat total allocations for a given row
|
||||||
if (!row.allocations) {
|
if (!row.allocations) {
|
||||||
|
row.allocated = 0;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,6 +374,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
quantity += item.quantity;
|
quantity += item.quantity;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
row.allocated = quantity;
|
||||||
|
|
||||||
return quantity;
|
return quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,7 +404,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
],
|
],
|
||||||
{
|
{
|
||||||
success: function(data) {
|
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(
|
var stock_input = constructField(
|
||||||
`items_stock_item_${pk}`,
|
`items_stock_item_${pk}`,
|
||||||
{
|
{
|
||||||
@ -872,11 +887,12 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
|||||||
<td id='part_${pk}'>
|
<td id='part_${pk}'>
|
||||||
${thumb} ${sub_part.full_name}
|
${thumb} ${sub_part.full_name}
|
||||||
</td>
|
</td>
|
||||||
|
<td id='allocated_${pk}'>
|
||||||
|
${allocated_display}
|
||||||
|
</td>
|
||||||
<td id='stock_item_${pk}'>
|
<td id='stock_item_${pk}'>
|
||||||
${stock_input}
|
${stock_input}
|
||||||
</td>
|
</td>
|
||||||
<td id='allocated_${pk}'>
|
|
||||||
</td>
|
|
||||||
<td id='quantity_${pk}'>
|
<td id='quantity_${pk}'>
|
||||||
${quantity_input}
|
${quantity_input}
|
||||||
</td>
|
</td>
|
||||||
@ -894,7 +910,15 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
|||||||
for (var idx = 0; idx < bom_items.length; idx++) {
|
for (var idx = 0; idx < bom_items.length; idx++) {
|
||||||
var bom_item = bom_items[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) {
|
if (bom_items.length == 0) {
|
||||||
@ -913,8 +937,8 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Part" %}</th>
|
<th>{% trans "Part" %}</th>
|
||||||
<th style='min-width: 250px;'>{% trans "Stock Item" %}</th>
|
|
||||||
<th>{% trans "Allocated" %}</th>
|
<th>{% trans "Allocated" %}</th>
|
||||||
|
<th style='min-width: 250px;'>{% trans "Stock Item" %}</th>
|
||||||
<th>{% trans "Quantity" %}</th>
|
<th>{% trans "Quantity" %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -942,7 +966,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
|||||||
name: `items_stock_item_${bom_item.pk}`,
|
name: `items_stock_item_${bom_item.pk}`,
|
||||||
api_url: '{% url "api-stock-list" %}',
|
api_url: '{% url "api-stock-list" %}',
|
||||||
filters: {
|
filters: {
|
||||||
part: bom_item.sub_part,
|
bom_item: bom_item.pk,
|
||||||
in_stock: true,
|
in_stock: true,
|
||||||
part_detail: false,
|
part_detail: false,
|
||||||
location_detail: true,
|
location_detail: true,
|
||||||
@ -968,7 +992,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
|||||||
$(options.modal).find(`#allocation_row_${pk}`).remove();
|
$(options.modal).find(`#allocation_row_${pk}`).remove();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSubmit: function(fields, options) {
|
onSubmit: function(fields, opts) {
|
||||||
|
|
||||||
// Extract elements from the form
|
// Extract elements from the form
|
||||||
var data = {
|
var data = {
|
||||||
@ -983,7 +1007,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
|||||||
`items_quantity_${item.pk}`,
|
`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}`,
|
`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
|
// Provide nested values
|
||||||
options.nested = {
|
opts.nested = {
|
||||||
"items": item_pk_values
|
"items": item_pk_values
|
||||||
};
|
};
|
||||||
|
|
||||||
inventreePut(
|
inventreePut(
|
||||||
options.url,
|
opts.url,
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
// Hide the modal
|
// Hide the modal
|
||||||
$(options.modal).modal('hide');
|
$(opts.modal).modal('hide');
|
||||||
|
|
||||||
if (options.success) {
|
if (options.success) {
|
||||||
options.success(response);
|
options.success(response);
|
||||||
@ -1027,10 +1051,10 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
|||||||
error: function(xhr) {
|
error: function(xhr) {
|
||||||
switch (xhr.status) {
|
switch (xhr.status) {
|
||||||
case 400:
|
case 400:
|
||||||
handleFormErrors(xhr.responseJSON, fields, options);
|
handleFormErrors(xhr.responseJSON, fields, opts);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$(options.modal).modal('hide');
|
$(opts.modal).modal('hide');
|
||||||
showApiError(xhr);
|
showApiError(xhr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user