mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Adds API endpoint for "auto allocating" stock items against a build order.
- If stock exists in multiple locations, and the user "does not care" where to take from, simply iterate through and take
This commit is contained in:
parent
004ced8030
commit
434f563a41
@ -322,6 +322,37 @@ class BuildFinish(generics.CreateAPIView):
|
||||
return ctx
|
||||
|
||||
|
||||
class BuildAutoAllocate(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for 'automatically' allocating stock against a build order.
|
||||
|
||||
- Only looks at 'untracked' parts
|
||||
- If stock exists in a single location, easy!
|
||||
- If user decides that stock items are "fungible", allocate against multiple stock items
|
||||
- If the user wants to, allocate substite parts if the primary parts are not available.
|
||||
"""
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
||||
serializer_class = build.serializers.BuildAutoAllocationSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""
|
||||
Provide the Build object to the serializer context
|
||||
"""
|
||||
|
||||
context = super().get_serializer_context()
|
||||
|
||||
try:
|
||||
context['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
|
||||
except:
|
||||
pass
|
||||
|
||||
context['request'] = self.request
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class BuildAllocate(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint to allocate stock items to a build order
|
||||
@ -477,6 +508,7 @@ build_api_urls = [
|
||||
# Build Detail
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
|
||||
url(r'^auto-allocate/', BuildAutoAllocate.as_view(), name='api-build-auto-allocate'),
|
||||
url(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'),
|
||||
url(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'),
|
||||
url(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'),
|
||||
|
@ -25,6 +25,8 @@ from markdownx.models import MarkdownxField
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from mptt.exceptions import InvalidMove
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
|
||||
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
|
||||
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
|
||||
@ -823,6 +825,86 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def auto_allocate_stock(self, user, **kwargs):
|
||||
"""
|
||||
Automatically allocate stock items against this build order,
|
||||
following a number of 'guidelines':
|
||||
|
||||
- Only "untracked" BOM items are considered (tracked BOM items must be manually allocated)
|
||||
- If a particular BOM item is already fully allocated, it is skipped
|
||||
- Extract all available stock items for the BOM part
|
||||
- If variant stock is allowed, extract stock for those too
|
||||
- If substitute parts are available, extract stock for those also
|
||||
- If a single stock item is found, we can allocate that and move on!
|
||||
- If multiple stock items are found, we *may* be able to allocate:
|
||||
- If the calling function has specified that items are interchangeable
|
||||
"""
|
||||
|
||||
location = kwargs.get('location', None)
|
||||
interchangeable = kwargs.get('interchangeable', False)
|
||||
substitutes = kwargs.get('substitutes', True)
|
||||
|
||||
# Get a list of all 'untracked' BOM items
|
||||
for bom_item in self.untracked_bom_items:
|
||||
|
||||
unallocated_quantity = self.unallocated_quantity(bom_item)
|
||||
|
||||
if unallocated_quantity <= 0:
|
||||
# This BomItem is fully allocated, we can continue
|
||||
continue
|
||||
|
||||
# Check which parts we can "use" (may include variants and substitutes)
|
||||
available_parts = bom_item.get_valid_parts_for_allocation(
|
||||
allow_variants=True,
|
||||
allow_substitutes=substitutes,
|
||||
)
|
||||
|
||||
# Look for available stock items
|
||||
available_stock = StockModels.StockItem.objects.filter(StockModels.StockItem.IN_STOCK_FILTER)
|
||||
|
||||
# Filter by list of available parts
|
||||
available_stock = available_stock.filter(
|
||||
part__in=[p for p in available_parts],
|
||||
)
|
||||
|
||||
if location:
|
||||
# Filter only stock items located "below" the specified location
|
||||
sublocations = location.get_descendants(include_self=True)
|
||||
available_stock = available_stock.filter(location__in=[loc for loc in sublocations])
|
||||
|
||||
if available_stock.count() == 0:
|
||||
# No stock items are available
|
||||
continue
|
||||
elif available_stock.count() == 1 or interchangeable:
|
||||
# Either there is only a single stock item available,
|
||||
# or all items are "interchangeable" and we don't care where we take stock from
|
||||
|
||||
for stock_item in available_stock:
|
||||
# How much of the stock item is "available" for allocation?
|
||||
quantity = min(unallocated_quantity, stock_item.unallocated_quantity())
|
||||
|
||||
if quantity > 0:
|
||||
|
||||
try:
|
||||
BuildItem.objects.create(
|
||||
build=self,
|
||||
bom_item=bom_item,
|
||||
stock_item=stock_item,
|
||||
quantity=quantity,
|
||||
)
|
||||
|
||||
# Subtract the required quantity
|
||||
unallocated_quantity -= quantity
|
||||
|
||||
except (ValidationError, serializers.ValidationError) as exc:
|
||||
# Catch model errors and re-throw as DRF errors
|
||||
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
||||
|
||||
if unallocated_quantity <= 0:
|
||||
# We have now fully-allocated this BomItem - no need to continue!
|
||||
break
|
||||
|
||||
def required_quantity(self, bom_item, output=None):
|
||||
"""
|
||||
Get the quantity of a part required to complete the particular build output.
|
||||
|
@ -709,6 +709,54 @@ class BuildAllocationSerializer(serializers.Serializer):
|
||||
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
||||
|
||||
|
||||
class BuildAutoAllocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for auto allocating stock items against a build order
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
'location',
|
||||
'interchangeable',
|
||||
'substitutes',
|
||||
]
|
||||
|
||||
location = serializers.PrimaryKeyRelatedField(
|
||||
queryset=StockLocation.objects.all(),
|
||||
many=False,
|
||||
allow_null=True,
|
||||
required=False,
|
||||
label=_('Source Location'),
|
||||
help_text=_('Stock location where parts are to be sourced (leave blank to take from any location)'),
|
||||
)
|
||||
|
||||
interchangeable = serializers.BooleanField(
|
||||
default=False,
|
||||
label=_('Interchangeable Stock'),
|
||||
help_text=_('Stock items in multiple locations can be used interchangeably'),
|
||||
)
|
||||
|
||||
substitutes = serializers.BooleanField(
|
||||
default=True,
|
||||
label=_('Substitute Stock'),
|
||||
help_text=_('Allow allocation of substitute parts'),
|
||||
)
|
||||
|
||||
def save(self):
|
||||
|
||||
data = self.validated_data
|
||||
|
||||
request = self.context['request']
|
||||
build = self.context['build']
|
||||
|
||||
build.auto_allocate_stock(
|
||||
request.user,
|
||||
location=data.get('location', None),
|
||||
interchangeable=data['interchangeable'],
|
||||
substitutes=data['substitutes'],
|
||||
)
|
||||
|
||||
|
||||
class BuildItemSerializer(InvenTreeModelSerializer):
|
||||
""" Serializes a BuildItem object """
|
||||
|
||||
|
@ -177,7 +177,10 @@
|
||||
<button class='btn btn-danger' type='button' id='btn-unallocate' title='{% trans "Unallocate stock" %}'>
|
||||
<span class='fas fa-minus-circle'></span> {% trans "Unallocate Stock" %}
|
||||
</button>
|
||||
<button class='btn btn-success' type='button' id='btn-auto-allocate' title='{% trans "Allocate stock to build" %}'>
|
||||
<button class='btn btn-primary' type='button' id='btn-auto-allocate' title='{% trans "Automatically allocate stock to build" %}'>
|
||||
<span class='fas fa-magic'></span> {% trans "Auto Allocate" %}
|
||||
</button>
|
||||
<button class='btn btn-success' type='button' id='btn-allocate' title='{% trans "Manually allocate stock to build" %}'>
|
||||
<span class='fas fa-sign-in-alt'></span> {% trans "Allocate Stock" %}
|
||||
</button>
|
||||
<!--
|
||||
@ -485,8 +488,22 @@ function reloadTable() {
|
||||
}
|
||||
|
||||
{% if build.active %}
|
||||
|
||||
$("#btn-auto-allocate").on('click', function() {
|
||||
|
||||
autoAllocateStockToBuild(
|
||||
{{ build.pk }},
|
||||
[],
|
||||
{
|
||||
{% if build.take_from %}
|
||||
location: {{ build.take_from.pk }},
|
||||
{% endif %}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#btn-allocate").on('click', function() {
|
||||
|
||||
var bom_items = $("#allocation-table-untracked").bootstrapTable("getData");
|
||||
|
||||
var incomplete_bom_items = [];
|
||||
|
@ -2651,7 +2651,7 @@ class BomItem(models.Model, DataImportMixin):
|
||||
def get_api_url():
|
||||
return reverse('api-bom-list')
|
||||
|
||||
def get_valid_parts_for_allocation(self):
|
||||
def get_valid_parts_for_allocation(self, allow_variants=True, allow_substitutes=True):
|
||||
"""
|
||||
Return a list of valid parts which can be allocated against this BomItem:
|
||||
|
||||
@ -2666,13 +2666,14 @@ class BomItem(models.Model, DataImportMixin):
|
||||
parts.add(self.sub_part)
|
||||
|
||||
# Variant parts (if allowed)
|
||||
if self.allow_variants:
|
||||
if allow_variants and 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)
|
||||
if allow_substitutes:
|
||||
for sub in self.substitutes.all():
|
||||
parts.add(sub.part)
|
||||
|
||||
return parts
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
/* exported
|
||||
allocateStockToBuild,
|
||||
autoAllocateStockToBuild,
|
||||
completeBuildOrder,
|
||||
createBuildOutput,
|
||||
editBuildOrder,
|
||||
@ -1844,6 +1845,34 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically allocate stock items to a build
|
||||
*/
|
||||
function autoAllocateStockToBuild(build_id, bom_items=[], options={}) {
|
||||
|
||||
var html = '';
|
||||
|
||||
var fields = {
|
||||
location: {
|
||||
value: options.location,
|
||||
},
|
||||
interchangeable: {},
|
||||
substitutes: {},
|
||||
}
|
||||
|
||||
constructForm(`/api/build/${build_id}/auto-allocate/`, {
|
||||
method: 'POST',
|
||||
fields: fields,
|
||||
title: '{% trans "Allocate Stock Items" %}',
|
||||
confirm: true,
|
||||
preFormContent: html,
|
||||
onSuccess: function(response) {
|
||||
// TODO - Reload the allocation table
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Display a table of Build orders
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user