diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py
index e6331f2b6a..10cc7e2024 100644
--- a/InvenTree/build/api.py
+++ b/InvenTree/build/api.py
@@ -11,7 +11,7 @@ from rest_framework import generics
from django.conf.urls import url, include
-from InvenTree.helpers import str2bool
+from InvenTree.helpers import str2bool, isNull
from InvenTree.status_codes import BuildStatus
from .models import Build, BuildItem
@@ -194,7 +194,11 @@ class BuildItemList(generics.ListCreateAPIView):
output = params.get('output', None)
if output:
- queryset = queryset.filter(install_into=output)
+
+ if isNull(output):
+ queryset = queryset.filter(install_into=None)
+ else:
+ queryset = queryset.filter(install_into=output)
return queryset
diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py
index 0726779b87..d4f5e23ce7 100644
--- a/InvenTree/build/forms.py
+++ b/InvenTree/build/forms.py
@@ -165,16 +165,10 @@ class AutoAllocateForm(HelperForm):
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Confirm stock allocation'))
- # Keep track of which build output we are interested in
- output = forms.ModelChoiceField(
- queryset=StockItem.objects.all(),
- )
-
class Meta:
model = Build
fields = [
'confirm',
- 'output',
]
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index 5a23752071..0fc92ce045 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -489,7 +489,7 @@ class Build(MPTTModel):
self.status = BuildStatus.CANCELLED
self.save()
- def getAutoAllocations(self, output):
+ def getAutoAllocations(self):
"""
Return a list of StockItem objects which will be allocated
using the 'AutoAllocate' function.
@@ -521,15 +521,19 @@ class Build(MPTTModel):
part = bom_item.sub_part
+ # If the part is "trackable" it cannot be auto-allocated
+ if part.trackable:
+ continue
+
# Skip any parts which are already fully allocated
- if self.isPartFullyAllocated(part, output):
+ if self.isPartFullyAllocated(part, None):
continue
# How many parts are required to complete the output?
- required = self.unallocatedQuantity(part, output)
+ required = self.unallocatedQuantity(part, None)
# Grab a list of stock items which are available
- stock_items = self.availableStockItems(part, output)
+ stock_items = self.availableStockItems(part, None)
# Ensure that the available stock items are in the correct location
if self.take_from is not None:
@@ -544,7 +548,6 @@ class Build(MPTTModel):
build_items = BuildItem.objects.filter(
build=self,
stock_item=stock_item,
- install_into=output
)
if len(build_items) > 0:
@@ -567,24 +570,45 @@ class Build(MPTTModel):
return allocations
@transaction.atomic
- def unallocateStock(self, output=None, part=None):
+ def unallocateOutput(self, output, part=None):
"""
- Deletes all stock allocations for this build.
-
- Args:
- output: Specify which build output to delete allocations (optional)
-
+ Unallocate all stock which are allocated against the provided "output" (StockItem)
"""
- allocations = BuildItem.objects.filter(build=self.pk)
-
- if output:
- allocations = allocations.filter(install_into=output.pk)
+ allocations = BuildItem.objects.filter(
+ build=self,
+ install_into=output
+ )
if part:
allocations = allocations.filter(stock_item__part=part)
- # Remove all the allocations
+ allocations.delete()
+
+ @transaction.atomic
+ def unallocateUntracked(self, part=None):
+ """
+ Unallocate all "untracked" stock
+ """
+
+ allocations = BuildItem.objects.filter(
+ build=self,
+ install_into=None
+ )
+
+ if part:
+ allocations = allocations.filter(stock_item__part=part)
+
+ allocations.delete()
+
+ @transaction.atomic
+ def unallocateAll(self):
+ """
+ Deletes all stock allocations for this build.
+ """
+
+ allocations = BuildItem.objects.filter(build=self)
+
allocations.delete()
@transaction.atomic
@@ -685,7 +709,7 @@ class Build(MPTTModel):
output.delete()
@transaction.atomic
- def autoAllocate(self, output):
+ def autoAllocate(self):
"""
Run auto-allocation routine to allocate StockItems to this Build.
@@ -702,7 +726,7 @@ class Build(MPTTModel):
See: getAutoAllocations()
"""
- allocations = self.getAutoAllocations(output)
+ allocations = self.getAutoAllocations()
for item in allocations:
# Create a new allocation
@@ -710,7 +734,7 @@ class Build(MPTTModel):
build=self,
stock_item=item['stock_item'],
quantity=item['quantity'],
- install_into=output,
+ install_into=None
)
build_item.save()
@@ -779,7 +803,7 @@ class Build(MPTTModel):
if output:
quantity *= output.quantity
else:
- quantity *= self.remaining
+ quantity *= self.quantity
return quantity
@@ -1020,10 +1044,12 @@ class BuildItem(models.Model):
errors = {}
- if not self.install_into:
- raise ValidationError(_('Build item must specify a build output'))
-
try:
+
+ # If the 'part' is trackable, then the 'install_into' field must be set!
+ if self.stock_item.part and self.stock_item.part.trackable and not self.install_into:
+ raise ValidationError(_('Build item must specify a build output, as master part is marked as trackable'))
+
# Allocated part must be in the BOM for the master part
if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False):
errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)]
diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html
index 1b66f17d0d..fb4a8200e7 100644
--- a/InvenTree/build/templates/build/allocate.html
+++ b/InvenTree/build/templates/build/allocate.html
@@ -38,6 +38,41 @@
{% for item in build.incomplete_outputs %}
@@ -66,6 +101,9 @@
part: {{ build.part.pk }},
};
+ // Load allocation table for un-tracked parts
+ loadBuildOutputAllocationTable(buildInfo, null);
+
{% for item in build.incomplete_outputs %}
// Get the build output as a javascript object
inventreeGet('{% url 'api-stock-detail' item.pk %}', {},
diff --git a/InvenTree/build/templates/build/navbar.html b/InvenTree/build/templates/build/navbar.html
index 5e27010861..383f3c843f 100644
--- a/InvenTree/build/templates/build/navbar.html
+++ b/InvenTree/build/templates/build/navbar.html
@@ -27,7 +27,7 @@
- {% trans "In Progress" %}
+ {% trans "Allocate Stock" %}
{% endif %}
diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py
index 15ced77130..c467d374ba 100644
--- a/InvenTree/build/views.py
+++ b/InvenTree/build/views.py
@@ -18,7 +18,7 @@ from stock.models import StockLocation, StockItem
from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
from InvenTree.views import InvenTreeRoleMixin
-from InvenTree.helpers import str2bool, extract_serial_numbers, normalize
+from InvenTree.helpers import str2bool, extract_serial_numbers, normalize, isNull
from InvenTree.status_codes import BuildStatus
@@ -98,16 +98,6 @@ class BuildAutoAllocate(AjaxUpdateView):
initials = super().get_initial()
- # Pointing to a particular build output?
- output = self.get_param('output')
-
- if output:
- try:
- output = StockItem.objects.get(pk=output)
- initials['output'] = output
- except (ValueError, StockItem.DoesNotExist):
- pass
-
return initials
def get_context_data(self, *args, **kwargs):
@@ -121,16 +111,7 @@ class BuildAutoAllocate(AjaxUpdateView):
form = self.get_form()
- output_id = form['output'].value()
-
- try:
- output = StockItem.objects.get(pk=output_id)
- except (ValueError, StockItem.DoesNotExist):
- output = None
-
- if output:
- context['output'] = output
- context['allocations'] = build.getAutoAllocations(output)
+ context['allocations'] = build.getAutoAllocations()
context['build'] = build
@@ -140,18 +121,11 @@ class BuildAutoAllocate(AjaxUpdateView):
form = super().get_form()
- if form['output'].value():
- # Hide the 'output' field
- form.fields['output'].widget = HiddenInput()
-
return form
def validate(self, build, form, **kwargs):
- output = form.cleaned_data.get('output', None)
-
- if not output:
- form.add_error(None, _('Build output must be specified'))
+ pass
def save(self, build, form, **kwargs):
"""
@@ -159,9 +133,7 @@ class BuildAutoAllocate(AjaxUpdateView):
perform auto-allocations
"""
- output = form.cleaned_data.get('output', None)
-
- build.autoAllocate(output)
+ build.autoAllocate()
def get_data(self):
return {
@@ -365,10 +337,16 @@ class BuildUnallocate(AjaxUpdateView):
output_id = request.POST.get('output_id', None)
- try:
- output = StockItem.objects.get(pk=output_id)
- except (ValueError, StockItem.DoesNotExist):
- output = None
+ if output_id:
+
+ # If a "null" output is provided, we are trying to unallocate "untracked" stock
+ if isNull(output_id):
+ output = None
+ else:
+ try:
+ output = StockItem.objects.get(pk=output_id)
+ except (ValueError, StockItem.DoesNotExist):
+ output = None
part_id = request.POST.get('part_id', None)
@@ -383,9 +361,19 @@ class BuildUnallocate(AjaxUpdateView):
form.add_error('confirm', _('Confirm unallocation of build stock'))
form.add_error(None, _('Check the confirmation box'))
else:
- build.unallocateStock(output=output, part=part)
+
valid = True
+ # Unallocate the entire build
+ if not output_id:
+ build.unallocateAll()
+ # Unallocate a single output
+ elif output:
+ build.unallocateOutput(output, part=part)
+ # Unallocate "untracked" parts
+ else:
+ build.unallocateUntracked(part=part)
+
data = {
'form_valid': valid,
}
diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js
index 539d1565aa..cdfd6756aa 100644
--- a/InvenTree/templates/js/build.js
+++ b/InvenTree/templates/js/build.js
@@ -37,7 +37,12 @@ function makeBuildOutputActionButtons(output, buildInfo) {
*/
var buildId = buildInfo.pk;
- var outputId = output.pk;
+
+ if (output) {
+ outputId = output.pk;
+ } else {
+ outputId = 'untracked';
+ }
var panel = `#allocation-panel-${outputId}`;
@@ -50,35 +55,40 @@ function makeBuildOutputActionButtons(output, buildInfo) {
var html = `
`;
- // Add a button to "auto allocate" against the build
- html += makeIconButton(
- 'fa-magic icon-blue', 'button-output-auto', outputId,
- '{% trans "Auto-allocate stock items to this output" %}',
- );
-
- // Add a button to "complete" the particular build output
- html += makeIconButton(
- 'fa-check icon-green', 'button-output-complete', outputId,
- '{% trans "Complete build output" %}',
- {
- //disabled: true
- }
- );
+ // "Auto" allocation only works for untracked stock items
+ if (!output) {
+ html += makeIconButton(
+ 'fa-magic icon-blue', 'button-output-auto', outputId,
+ '{% trans "Auto-allocate stock items to this output" %}',
+ );
+ }
// Add a button to "cancel" the particular build output (unallocate)
html += makeIconButton(
'fa-minus-circle icon-red', 'button-output-unallocate', outputId,
'{% trans "Unallocate stock from build output" %}',
- );
+ );
- // Add a button to "delete" the particular build output
- html += makeIconButton(
- 'fa-trash-alt icon-red', 'button-output-delete', outputId,
- '{% trans "Delete build output" %}',
- );
- // Add a button to "destroy" the particular build output (mark as damaged, scrap)
- // TODO
+ if (output) {
+
+ // Add a button to "complete" the particular build output
+ html += makeIconButton(
+ 'fa-check icon-green', 'button-output-complete', outputId,
+ '{% trans "Complete build output" %}',
+ {
+ //disabled: true
+ }
+ );
+
+ // Add a button to "delete" the particular build output
+ html += makeIconButton(
+ 'fa-trash-alt icon-red', 'button-output-delete', outputId,
+ '{% trans "Delete build output" %}',
+ );
+
+ // TODO - Add a button to "destroy" the particular build output (mark as damaged, scrap)
+ }
html += '
';
@@ -90,7 +100,6 @@ function makeBuildOutputActionButtons(output, buildInfo) {
launchModalForm(`/build/${buildId}/auto-allocate/`,
{
data: {
- output: outputId,
},
success: reloadTable,
}
@@ -115,7 +124,7 @@ function makeBuildOutputActionButtons(output, buildInfo) {
{
success: reloadTable,
data: {
- output: outputId,
+ output: output ? outputId : 'null',
}
}
);
@@ -152,13 +161,21 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var outputId = null;
- outputId = output.pk;
+ if (output) {
+ outputId = output.pk;
+ } else {
+ outputId = 'untracked';
+ }
var table = options.table;
if (options.table == null) {
table = `#allocation-table-${outputId}`;
}
+
+ // If an "output" is specified, then only "trackable" parts are allocated
+ // Otherwise, only "untrackable" parts are allowed
+ var trackable = ! !output;
function reloadTable() {
// Reload the entire build allocation table
@@ -168,7 +185,13 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
function requiredQuantity(row) {
// Return the requied quantity for a given row
- return row.quantity * output.quantity;
+ if (output) {
+ // "Tracked" parts are calculated against individual build outputs
+ return row.quantity * output.quantity;
+ } else {
+ // "Untracked" parts are specified against the build itself
+ return row.quantity * buildInfo.quantity;
+ }
}
function sumAllocations(row) {
@@ -300,6 +323,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
queryParams: {
part: partId,
sub_part_detail: true,
+ sub_part_trackable: trackable,
},
formatNoMatches: function() {
return '{% trans "No BOM items found" %}';
@@ -310,11 +334,19 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
onLoadSuccess: function(tableData) {
// Once the BOM data are loaded, request allocation data for this build output
+ var params = {
+ build: buildId,
+ }
+
+ if (output) {
+ params.sub_part_trackable = true;
+ params.output = outputId;
+ } else {
+ params.sub_part_trackable = false;
+ }
+
inventreeGet('/api/build/item/',
- {
- build: buildId,
- output: outputId,
- },
+ params,
{
success: function(data) {
// Iterate through the returned data, and group by the part they point to
@@ -355,8 +387,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
// Calculate the total allocated quantity
var allocatedQuantity = sumAllocations(tableRow);
+ var requiredQuantity = 0;
+
+ if (output) {
+ requiredQuantity = tableRow.quantity * output.quantity;
+ } else {
+ requiredQuantity = tableRow.quantity * buildInfo.quantity;
+ }
+
// Is this line item fully allocated?
- if (allocatedQuantity >= (tableRow.quantity * output.quantity)) {
+ if (allocatedQuantity >= requiredQuantity) {
allocatedLines += 1;
}