mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into matmair/issue2694
This commit is contained in:
commit
b9ee436110
@ -12,11 +12,15 @@ import common.models
|
||||
INVENTREE_SW_VERSION = "0.7.0 dev"
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 27
|
||||
INVENTREE_API_VERSION = 28
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v28 -> 2022-03-04
|
||||
- Adds an API endpoint for auto allocation of stock items against a build order
|
||||
- Ref: https://github.com/inventree/InvenTree/pull/2713
|
||||
|
||||
v27 -> 2022-02-28
|
||||
- Adds target_date field to individual line items for purchase orders and sales orders
|
||||
|
||||
|
@ -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,106 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def auto_allocate_stock(self, **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:
|
||||
|
||||
variant_parts = bom_item.sub_part.get_descendants(include_self=False)
|
||||
|
||||
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])
|
||||
|
||||
"""
|
||||
Next, we sort the available stock items with the following priority:
|
||||
1. Direct part matches (+1)
|
||||
2. Variant part matches (+2)
|
||||
3. Substitute part matches (+3)
|
||||
|
||||
This ensures that allocation priority is first given to "direct" parts
|
||||
"""
|
||||
def stock_sort(item):
|
||||
if item.part == bom_item.sub_part:
|
||||
return 1
|
||||
elif item.part in variant_parts:
|
||||
return 2
|
||||
else:
|
||||
return 3
|
||||
|
||||
available_stock = sorted(available_stock, key=stock_sort)
|
||||
|
||||
if len(available_stock) == 0:
|
||||
# No stock items are available
|
||||
continue
|
||||
elif len(available_stock) == 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,52 @@ 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
|
||||
|
||||
build = self.context['build']
|
||||
|
||||
build.auto_allocate_stock(
|
||||
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 = [];
|
||||
|
@ -8,11 +8,11 @@ from django.db.utils import IntegrityError
|
||||
from InvenTree import status_codes as status
|
||||
|
||||
from build.models import Build, BuildItem, get_next_build_number
|
||||
from part.models import Part, BomItem
|
||||
from part.models import Part, BomItem, BomItemSubstitute
|
||||
from stock.models import StockItem
|
||||
|
||||
|
||||
class BuildTest(TestCase):
|
||||
class BuildTestBase(TestCase):
|
||||
"""
|
||||
Run some tests to ensure that the Build model is working properly.
|
||||
"""
|
||||
@ -107,13 +107,20 @@ class BuildTest(TestCase):
|
||||
)
|
||||
|
||||
# Create some stock items to assign to the build
|
||||
self.stock_1_1 = StockItem.objects.create(part=self.sub_part_1, quantity=1000)
|
||||
self.stock_1_1 = StockItem.objects.create(part=self.sub_part_1, quantity=3)
|
||||
self.stock_1_2 = StockItem.objects.create(part=self.sub_part_1, quantity=100)
|
||||
|
||||
self.stock_2_1 = StockItem.objects.create(part=self.sub_part_2, quantity=5000)
|
||||
self.stock_2_1 = StockItem.objects.create(part=self.sub_part_2, quantity=5)
|
||||
self.stock_2_2 = StockItem.objects.create(part=self.sub_part_2, quantity=5)
|
||||
self.stock_2_2 = StockItem.objects.create(part=self.sub_part_2, quantity=5)
|
||||
self.stock_2_2 = StockItem.objects.create(part=self.sub_part_2, quantity=5)
|
||||
self.stock_2_2 = StockItem.objects.create(part=self.sub_part_2, quantity=5)
|
||||
|
||||
self.stock_3_1 = StockItem.objects.create(part=self.sub_part_3, quantity=1000)
|
||||
|
||||
|
||||
class BuildTest(BuildTestBase):
|
||||
|
||||
def test_ref_int(self):
|
||||
"""
|
||||
Test the "integer reference" field used for natural sorting
|
||||
@ -137,7 +144,7 @@ class BuildTest(TestCase):
|
||||
def test_init(self):
|
||||
# Perform some basic tests before we start the ball rolling
|
||||
|
||||
self.assertEqual(StockItem.objects.count(), 6)
|
||||
self.assertEqual(StockItem.objects.count(), 10)
|
||||
|
||||
# Build is PENDING
|
||||
self.assertEqual(self.build.status, status.BuildStatus.PENDING)
|
||||
@ -183,7 +190,7 @@ class BuildTest(TestCase):
|
||||
b.clean()
|
||||
|
||||
# Ok, what about we make one that does *not* fail?
|
||||
b = BuildItem(stock_item=self.stock_1_1, build=self.build, install_into=self.output_1, quantity=10)
|
||||
b = BuildItem(stock_item=self.stock_1_2, build=self.build, install_into=self.output_1, quantity=10)
|
||||
b.save()
|
||||
|
||||
def test_duplicate_bom_line(self):
|
||||
@ -274,11 +281,14 @@ class BuildTest(TestCase):
|
||||
|
||||
self.assertFalse(self.build.are_untracked_parts_allocated())
|
||||
|
||||
self.stock_2_1.quantity = 500
|
||||
self.stock_2_1.save()
|
||||
|
||||
# Now we "fully" allocate the untracked untracked items
|
||||
self.allocate_stock(
|
||||
None,
|
||||
{
|
||||
self.stock_1_1: 50,
|
||||
self.stock_1_2: 50,
|
||||
self.stock_2_1: 50,
|
||||
}
|
||||
)
|
||||
@ -305,6 +315,12 @@ class BuildTest(TestCase):
|
||||
Test completion of a build output
|
||||
"""
|
||||
|
||||
self.stock_1_1.quantity = 1000
|
||||
self.stock_1_1.save()
|
||||
|
||||
self.stock_2_1.quantity = 30
|
||||
self.stock_2_1.save()
|
||||
|
||||
# Allocate non-tracked parts
|
||||
self.allocate_stock(
|
||||
None,
|
||||
@ -351,16 +367,15 @@ class BuildTest(TestCase):
|
||||
self.assertEqual(BuildItem.objects.count(), 0)
|
||||
|
||||
# New stock items should have been created!
|
||||
self.assertEqual(StockItem.objects.count(), 7)
|
||||
self.assertEqual(StockItem.objects.count(), 10)
|
||||
|
||||
# This stock item has been depleted!
|
||||
with self.assertRaises(StockItem.DoesNotExist):
|
||||
StockItem.objects.get(pk=self.stock_1_1.pk)
|
||||
|
||||
# This stock item has *not* been depleted
|
||||
x = StockItem.objects.get(pk=self.stock_2_1.pk)
|
||||
|
||||
self.assertEqual(x.quantity, 4970)
|
||||
# This stock item has also been depleted
|
||||
with self.assertRaises(StockItem.DoesNotExist):
|
||||
StockItem.objects.get(pk=self.stock_2_1.pk)
|
||||
|
||||
# And 10 new stock items created for the build output
|
||||
outputs = StockItem.objects.filter(build=self.build)
|
||||
@ -369,3 +384,108 @@ class BuildTest(TestCase):
|
||||
|
||||
for output in outputs:
|
||||
self.assertFalse(output.is_building)
|
||||
|
||||
|
||||
class AutoAllocationTests(BuildTestBase):
|
||||
"""
|
||||
Tests for auto allocating stock against a build order
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
# Add a "substitute" part for bom_item_2
|
||||
alt_part = Part.objects.create(
|
||||
name="alt part",
|
||||
description="An alternative part!",
|
||||
component=True,
|
||||
)
|
||||
|
||||
BomItemSubstitute.objects.create(
|
||||
bom_item=self.bom_item_2,
|
||||
part=alt_part,
|
||||
)
|
||||
|
||||
StockItem.objects.create(
|
||||
part=alt_part,
|
||||
quantity=500,
|
||||
)
|
||||
|
||||
def test_auto_allocate(self):
|
||||
"""
|
||||
Run the 'auto-allocate' function. What do we expect to happen?
|
||||
|
||||
There are two "untracked" parts:
|
||||
- sub_part_1 (quantity 5 per BOM = 50 required total) / 103 in stock (2 items)
|
||||
- sub_part_2 (quantity 3 per BOM = 30 required total) / 25 in stock (5 items)
|
||||
|
||||
A "fully auto" allocation should allocate *all* of these stock items to the build
|
||||
"""
|
||||
|
||||
# No build item allocations have been made against the build
|
||||
self.assertEqual(self.build.allocated_stock.count(), 0)
|
||||
|
||||
self.assertFalse(self.build.are_untracked_parts_allocated())
|
||||
|
||||
# Stock is not interchangeable, nothing will happen
|
||||
self.build.auto_allocate_stock(
|
||||
interchangeable=False,
|
||||
substitutes=False,
|
||||
)
|
||||
|
||||
self.assertFalse(self.build.are_untracked_parts_allocated())
|
||||
|
||||
self.assertEqual(self.build.allocated_stock.count(), 0)
|
||||
|
||||
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_1))
|
||||
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_2))
|
||||
|
||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 50)
|
||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 30)
|
||||
|
||||
# This time we expect stock to be allocated!
|
||||
self.build.auto_allocate_stock(
|
||||
interchangeable=True,
|
||||
substitutes=False,
|
||||
)
|
||||
|
||||
self.assertFalse(self.build.are_untracked_parts_allocated())
|
||||
|
||||
self.assertEqual(self.build.allocated_stock.count(), 7)
|
||||
|
||||
self.assertTrue(self.build.is_bom_item_allocated(self.bom_item_1))
|
||||
self.assertFalse(self.build.is_bom_item_allocated(self.bom_item_2))
|
||||
|
||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0)
|
||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 5)
|
||||
|
||||
# This time, allow substitue parts to be used!
|
||||
self.build.auto_allocate_stock(
|
||||
interchangeable=True,
|
||||
substitutes=True,
|
||||
)
|
||||
|
||||
# self.assertTrue(self.build.are_untracked_parts_allocated())
|
||||
|
||||
# self.assertEqual(self.build.allocated_stock.count(), 8)
|
||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0)
|
||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 0)
|
||||
|
||||
self.assertTrue(self.build.is_bom_item_allocated(self.bom_item_1))
|
||||
self.assertTrue(self.build.is_bom_item_allocated(self.bom_item_2))
|
||||
|
||||
def test_fully_auto(self):
|
||||
"""
|
||||
We should be able to auto-allocate against a build in a single go
|
||||
"""
|
||||
|
||||
self.build.auto_allocate_stock(
|
||||
interchangeable=True,
|
||||
substitutes=True
|
||||
)
|
||||
|
||||
self.assertTrue(self.build.are_untracked_parts_allocated())
|
||||
|
||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0)
|
||||
self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 0)
|
||||
|
@ -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
|
||||
|
||||
|
@ -244,7 +244,7 @@
|
||||
|
||||
{% for allocation in item.allocations.all %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% object_link 'build-detail' allocation.build.id allocation.build %}
|
||||
{% object_link 'build-detail' allocation.build.id allocation.build as link %}
|
||||
{% decimal allocation.quantity as qty %}
|
||||
{% trans "This stock item is allocated to Build Order" %} {{ link }} {% if qty < item.quantity %}({% trans "Quantity" %}: {{ qty }}){% endif %}
|
||||
</div>
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
/* exported
|
||||
allocateStockToBuild,
|
||||
autoAllocateStockToBuild,
|
||||
completeBuildOrder,
|
||||
createBuildOutput,
|
||||
editBuildOrder,
|
||||
@ -754,7 +755,7 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
filters[key] = params[key];
|
||||
}
|
||||
|
||||
// TODO: Initialize filter list
|
||||
setupFilterList('builditems', $(table), options.filterTarget || '#filter-list-incompletebuilditems');
|
||||
|
||||
function setupBuildOutputButtonCallbacks() {
|
||||
|
||||
@ -999,7 +1000,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
filters[key] = params[key];
|
||||
}
|
||||
|
||||
setupFilterList('builditems', $(table), options.filterTarget || null);
|
||||
setupFilterList('builditems', $(table), options.filterTarget);
|
||||
|
||||
// If an "output" is specified, then only "trackable" parts are allocated
|
||||
// Otherwise, only "untrackable" parts are allowed
|
||||
@ -1512,6 +1513,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
*/
|
||||
function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
||||
|
||||
if (bom_items.length == 0) {
|
||||
|
||||
showAlertDialog(
|
||||
'{% trans "Select Parts" %}',
|
||||
'{% trans "You must select at least one part to allocate" %}',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// ID of the associated "build output" (or null)
|
||||
var output_id = options.output || null;
|
||||
|
||||
@ -1626,8 +1637,8 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
||||
if (table_entries.length == 0) {
|
||||
|
||||
showAlertDialog(
|
||||
'{% trans "Select Parts" %}',
|
||||
'{% trans "You must select at least one part to allocate" %}',
|
||||
'{% trans "All Parts Allocated" %}',
|
||||
'{% trans "All selected parts have been fully allocated" %}',
|
||||
);
|
||||
|
||||
return;
|
||||
@ -1844,6 +1855,48 @@ 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 = `
|
||||
<div class='alert alert-block alert-info'>
|
||||
<strong>{% trans "Automatic Stock Allocation" %}</strong><br>
|
||||
{% trans "Stock items will be automatically allocated to this build order, according to the provided guidelines" %}:
|
||||
<ul>
|
||||
<li>{% trans "If a location is specifed, stock will only be allocated from that location" %}</li>
|
||||
<li>{% trans "If stock is considered interchangeable, it will be allocated from the first location it is found" %}</li>
|
||||
<li>{% trans "If substitute stock is allowed, it will be used where stock of the primary part cannot be found" %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
var fields = {
|
||||
location: {
|
||||
value: options.location,
|
||||
},
|
||||
interchangeable: {
|
||||
value: true,
|
||||
},
|
||||
substitutes: {
|
||||
value: true,
|
||||
},
|
||||
};
|
||||
|
||||
constructForm(`/api/build/${build_id}/auto-allocate/`, {
|
||||
method: 'POST',
|
||||
fields: fields,
|
||||
title: '{% trans "Allocate Stock Items" %}',
|
||||
confirm: true,
|
||||
preFormContent: html,
|
||||
onSuccess: function(response) {
|
||||
$('#allocation-table-untracked').bootstrapTable('refresh');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Display a table of Build orders
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user