Add separate section for "untracked" part allocation

This commit is contained in:
Oliver Walters 2021-04-18 21:41:34 +10:00
parent 852cc9c4fa
commit 9e470d4064
7 changed files with 191 additions and 101 deletions

View File

@ -11,7 +11,7 @@ from rest_framework import generics
from django.conf.urls import url, include 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 InvenTree.status_codes import BuildStatus
from .models import Build, BuildItem from .models import Build, BuildItem
@ -194,7 +194,11 @@ class BuildItemList(generics.ListCreateAPIView):
output = params.get('output', None) output = params.get('output', None)
if output: 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 return queryset

View File

@ -165,16 +165,10 @@ class AutoAllocateForm(HelperForm):
confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Confirm stock allocation')) 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: class Meta:
model = Build model = Build
fields = [ fields = [
'confirm', 'confirm',
'output',
] ]

View File

@ -489,7 +489,7 @@ class Build(MPTTModel):
self.status = BuildStatus.CANCELLED self.status = BuildStatus.CANCELLED
self.save() self.save()
def getAutoAllocations(self, output): def getAutoAllocations(self):
""" """
Return a list of StockItem objects which will be allocated Return a list of StockItem objects which will be allocated
using the 'AutoAllocate' function. using the 'AutoAllocate' function.
@ -521,15 +521,19 @@ class Build(MPTTModel):
part = bom_item.sub_part 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 # Skip any parts which are already fully allocated
if self.isPartFullyAllocated(part, output): if self.isPartFullyAllocated(part, None):
continue continue
# How many parts are required to complete the output? # 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 # 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 # Ensure that the available stock items are in the correct location
if self.take_from is not None: if self.take_from is not None:
@ -544,7 +548,6 @@ class Build(MPTTModel):
build_items = BuildItem.objects.filter( build_items = BuildItem.objects.filter(
build=self, build=self,
stock_item=stock_item, stock_item=stock_item,
install_into=output
) )
if len(build_items) > 0: if len(build_items) > 0:
@ -567,24 +570,45 @@ class Build(MPTTModel):
return allocations return allocations
@transaction.atomic @transaction.atomic
def unallocateStock(self, output=None, part=None): def unallocateOutput(self, output, part=None):
""" """
Deletes all stock allocations for this build. Unallocate all stock which are allocated against the provided "output" (StockItem)
Args:
output: Specify which build output to delete allocations (optional)
""" """
allocations = BuildItem.objects.filter(build=self.pk) allocations = BuildItem.objects.filter(
build=self,
if output: install_into=output
allocations = allocations.filter(install_into=output.pk) )
if part: if part:
allocations = allocations.filter(stock_item__part=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() allocations.delete()
@transaction.atomic @transaction.atomic
@ -685,7 +709,7 @@ class Build(MPTTModel):
output.delete() output.delete()
@transaction.atomic @transaction.atomic
def autoAllocate(self, output): def autoAllocate(self):
""" """
Run auto-allocation routine to allocate StockItems to this Build. Run auto-allocation routine to allocate StockItems to this Build.
@ -702,7 +726,7 @@ class Build(MPTTModel):
See: getAutoAllocations() See: getAutoAllocations()
""" """
allocations = self.getAutoAllocations(output) allocations = self.getAutoAllocations()
for item in allocations: for item in allocations:
# Create a new allocation # Create a new allocation
@ -710,7 +734,7 @@ class Build(MPTTModel):
build=self, build=self,
stock_item=item['stock_item'], stock_item=item['stock_item'],
quantity=item['quantity'], quantity=item['quantity'],
install_into=output, install_into=None
) )
build_item.save() build_item.save()
@ -779,7 +803,7 @@ class Build(MPTTModel):
if output: if output:
quantity *= output.quantity quantity *= output.quantity
else: else:
quantity *= self.remaining quantity *= self.quantity
return quantity return quantity
@ -1020,10 +1044,12 @@ class BuildItem(models.Model):
errors = {} errors = {}
if not self.install_into:
raise ValidationError(_('Build item must specify a build output'))
try: 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 # Allocated part must be in the BOM for the master part
if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False): 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)] errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'").format(p=self.build.part.full_name)]

View File

@ -38,6 +38,41 @@
</div> </div>
<hr> <hr>
<div class="panel panel-default" id='allocation-panel-untracked'>
<div class="panel-heading" role="tab" id="heading-untracked">
<div class="panel-title">
<div class='row'>
<a class='collapsed' aria-expanded='false' role="button" data-toggle="collapse" data-parent="#build-output-accordion" href="#collapse-untracked" aria-controls="collapse-untracked">
<div class='col-sm-6'>
<span class='fas fa-caret-right'></span>
{% trans "Allocated Stock" %}
</div>
</a>
<div class='col-sm-3'>
<div>
<div id='output-progress-untracked'>
<span class='fas fa-spin fa-spinner'></span>
</div>
</div>
</div>
<div class='col-sm-3'>
<div class='btn-group float-right' id='output-actions-untracked'>
<span class='fas fa-spin fa-spinner'></span>
</div>
</div>
</div>
</div>
</div>
<div id="collapse-untracked" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-untracked">
<div class="panel-body">
<table class='table table-striped table-condensed' id='allocation-table-untracked'></table>
</div>
</div>
</div>
<hr>
{% if build.incomplete_outputs %} {% if build.incomplete_outputs %}
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true"> <div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
{% for item in build.incomplete_outputs %} {% for item in build.incomplete_outputs %}
@ -66,6 +101,9 @@
part: {{ build.part.pk }}, part: {{ build.part.pk }},
}; };
// Load allocation table for un-tracked parts
loadBuildOutputAllocationTable(buildInfo, null);
{% for item in build.incomplete_outputs %} {% for item in build.incomplete_outputs %}
// Get the build output as a javascript object // Get the build output as a javascript object
inventreeGet('{% url 'api-stock-detail' item.pk %}', {}, inventreeGet('{% url 'api-stock-detail' item.pk %}', {},

View File

@ -27,7 +27,7 @@
<li class='list-group-item {% if tab == "allocate" %}active{% endif %}' title='{% trans "In Progress" %}'> <li class='list-group-item {% if tab == "allocate" %}active{% endif %}' title='{% trans "In Progress" %}'>
<a href='{% url "build-allocate" build.id %}'> <a href='{% url "build-allocate" build.id %}'>
<span class='fas fa-tools'></span> <span class='fas fa-tools'></span>
{% trans "In Progress" %} {% trans "Allocate Stock" %}
</a> </a>
</li> </li>
{% endif %} {% endif %}

View File

@ -18,7 +18,7 @@ from stock.models import StockLocation, StockItem
from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView
from InvenTree.views import InvenTreeRoleMixin 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 from InvenTree.status_codes import BuildStatus
@ -98,16 +98,6 @@ class BuildAutoAllocate(AjaxUpdateView):
initials = super().get_initial() 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 return initials
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
@ -121,16 +111,7 @@ class BuildAutoAllocate(AjaxUpdateView):
form = self.get_form() form = self.get_form()
output_id = form['output'].value() context['allocations'] = build.getAutoAllocations()
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['build'] = build context['build'] = build
@ -140,18 +121,11 @@ class BuildAutoAllocate(AjaxUpdateView):
form = super().get_form() form = super().get_form()
if form['output'].value():
# Hide the 'output' field
form.fields['output'].widget = HiddenInput()
return form return form
def validate(self, build, form, **kwargs): def validate(self, build, form, **kwargs):
output = form.cleaned_data.get('output', None) pass
if not output:
form.add_error(None, _('Build output must be specified'))
def save(self, build, form, **kwargs): def save(self, build, form, **kwargs):
""" """
@ -159,9 +133,7 @@ class BuildAutoAllocate(AjaxUpdateView):
perform auto-allocations perform auto-allocations
""" """
output = form.cleaned_data.get('output', None) build.autoAllocate()
build.autoAllocate(output)
def get_data(self): def get_data(self):
return { return {
@ -365,10 +337,16 @@ class BuildUnallocate(AjaxUpdateView):
output_id = request.POST.get('output_id', None) output_id = request.POST.get('output_id', None)
try: if output_id:
output = StockItem.objects.get(pk=output_id)
except (ValueError, StockItem.DoesNotExist): # If a "null" output is provided, we are trying to unallocate "untracked" stock
output = None 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) 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('confirm', _('Confirm unallocation of build stock'))
form.add_error(None, _('Check the confirmation box')) form.add_error(None, _('Check the confirmation box'))
else: else:
build.unallocateStock(output=output, part=part)
valid = True 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 = { data = {
'form_valid': valid, 'form_valid': valid,
} }

View File

@ -37,7 +37,12 @@ function makeBuildOutputActionButtons(output, buildInfo) {
*/ */
var buildId = buildInfo.pk; var buildId = buildInfo.pk;
var outputId = output.pk;
if (output) {
outputId = output.pk;
} else {
outputId = 'untracked';
}
var panel = `#allocation-panel-${outputId}`; var panel = `#allocation-panel-${outputId}`;
@ -50,35 +55,40 @@ function makeBuildOutputActionButtons(output, buildInfo) {
var html = `<div class='btn-group float-right' role='group'>`; var html = `<div class='btn-group float-right' role='group'>`;
// Add a button to "auto allocate" against the build // "Auto" allocation only works for untracked stock items
html += makeIconButton( if (!output) {
'fa-magic icon-blue', 'button-output-auto', outputId, html += makeIconButton(
'{% trans "Auto-allocate stock items to this output" %}', '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
}
);
// Add a button to "cancel" the particular build output (unallocate) // Add a button to "cancel" the particular build output (unallocate)
html += makeIconButton( html += makeIconButton(
'fa-minus-circle icon-red', 'button-output-unallocate', outputId, 'fa-minus-circle icon-red', 'button-output-unallocate', outputId,
'{% trans "Unallocate stock from build output" %}', '{% 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) if (output) {
// TODO
// 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 += '</div>'; html += '</div>';
@ -90,7 +100,6 @@ function makeBuildOutputActionButtons(output, buildInfo) {
launchModalForm(`/build/${buildId}/auto-allocate/`, launchModalForm(`/build/${buildId}/auto-allocate/`,
{ {
data: { data: {
output: outputId,
}, },
success: reloadTable, success: reloadTable,
} }
@ -115,7 +124,7 @@ function makeBuildOutputActionButtons(output, buildInfo) {
{ {
success: reloadTable, success: reloadTable,
data: { data: {
output: outputId, output: output ? outputId : 'null',
} }
} }
); );
@ -152,13 +161,21 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var outputId = null; var outputId = null;
outputId = output.pk; if (output) {
outputId = output.pk;
} else {
outputId = 'untracked';
}
var table = options.table; var table = options.table;
if (options.table == null) { if (options.table == null) {
table = `#allocation-table-${outputId}`; 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() { function reloadTable() {
// Reload the entire build allocation table // Reload the entire build allocation table
@ -168,7 +185,13 @@ 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
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) { function sumAllocations(row) {
@ -300,6 +323,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
queryParams: { queryParams: {
part: partId, part: partId,
sub_part_detail: true, sub_part_detail: true,
sub_part_trackable: trackable,
}, },
formatNoMatches: function() { formatNoMatches: function() {
return '{% trans "No BOM items found" %}'; return '{% trans "No BOM items found" %}';
@ -310,11 +334,19 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
onLoadSuccess: function(tableData) { onLoadSuccess: function(tableData) {
// Once the BOM data are loaded, request allocation data for this build output // 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/', inventreeGet('/api/build/item/',
{ params,
build: buildId,
output: outputId,
},
{ {
success: function(data) { success: function(data) {
// Iterate through the returned data, and group by the part they point to // 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 // Calculate the total allocated quantity
var allocatedQuantity = sumAllocations(tableRow); 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? // Is this line item fully allocated?
if (allocatedQuantity >= (tableRow.quantity * output.quantity)) { if (allocatedQuantity >= requiredQuantity) {
allocatedLines += 1; allocatedLines += 1;
} }