mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Move "build unallocate" functionality to the API
- Much much simpler now! - Filtering is against bom_item, not part - Fixes a bug with the new (reasonably complex) substitution framework
This commit is contained in:
parent
1cbce5dfbf
commit
7dfffcd5d3
@ -21,7 +21,7 @@ from InvenTree.status_codes import BuildStatus
|
||||
|
||||
from .models import Build, BuildItem, BuildOrderAttachment
|
||||
from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer
|
||||
from .serializers import BuildAllocationSerializer
|
||||
from .serializers import BuildAllocationSerializer, BuildUnallocationSerializer
|
||||
|
||||
|
||||
class BuildFilter(rest_filters.FilterSet):
|
||||
@ -184,6 +184,42 @@ class BuildDetail(generics.RetrieveUpdateAPIView):
|
||||
serializer_class = BuildSerializer
|
||||
|
||||
|
||||
class BuildUnallocate(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for unallocating stock items from a build order
|
||||
|
||||
- The BuildOrder object is specified by the URL
|
||||
- "output" (StockItem) can optionally be specified
|
||||
- "bom_item" can optionally be specified
|
||||
"""
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
||||
serializer_class = BuildUnallocationSerializer
|
||||
|
||||
def get_build(self):
|
||||
"""
|
||||
Returns the BuildOrder associated with this API endpoint
|
||||
"""
|
||||
|
||||
pk = self.kwargs.get('pk', None)
|
||||
|
||||
try:
|
||||
build = Build.objects.get(pk=pk)
|
||||
except (ValueError, Build.DoesNotExist):
|
||||
raise ValidationError(_("Matching build order does not exist"))
|
||||
|
||||
return build
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['build'] = self.get_build()
|
||||
ctx['request'] = self.request
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class BuildAllocate(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint to allocate stock items to a build order
|
||||
@ -349,6 +385,7 @@ build_api_urls = [
|
||||
# Build Detail
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'),
|
||||
url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'),
|
||||
url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
|
||||
])),
|
||||
|
||||
|
@ -137,32 +137,6 @@ class BuildOutputDeleteForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class UnallocateBuildForm(HelperForm):
|
||||
"""
|
||||
Form for auto-de-allocation of stock from a build
|
||||
"""
|
||||
|
||||
confirm = forms.BooleanField(required=False, label=_('Confirm'), help_text=_('Confirm unallocation of stock'))
|
||||
|
||||
output_id = forms.IntegerField(
|
||||
required=False,
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
|
||||
part_id = forms.IntegerField(
|
||||
required=False,
|
||||
widget=forms.HiddenInput(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Build
|
||||
fields = [
|
||||
'confirm',
|
||||
'output_id',
|
||||
'part_id',
|
||||
]
|
||||
|
||||
|
||||
class CompleteBuildForm(HelperForm):
|
||||
"""
|
||||
Form for marking a build as complete
|
||||
|
@ -587,9 +587,13 @@ class Build(MPTTModel):
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def unallocateOutput(self, output, part=None):
|
||||
def unallocateStock(self, bom_item=None, output=None):
|
||||
"""
|
||||
Unallocate all stock which are allocated against the provided "output" (StockItem)
|
||||
Unallocate stock from this Build
|
||||
|
||||
arguments:
|
||||
- bom_item: Specify a particular BomItem to unallocate stock against
|
||||
- output: Specify a particular StockItem (output) to unallocate stock against
|
||||
"""
|
||||
|
||||
allocations = BuildItem.objects.filter(
|
||||
@ -597,34 +601,8 @@ class Build(MPTTModel):
|
||||
install_into=output
|
||||
)
|
||||
|
||||
if part:
|
||||
allocations = allocations.filter(stock_item__part=part)
|
||||
|
||||
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)
|
||||
if bom_item:
|
||||
allocations = allocations.filter(bom_item=bom_item)
|
||||
|
||||
allocations.delete()
|
||||
|
||||
|
@ -120,6 +120,61 @@ class BuildSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class BuildUnallocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for unallocating stock from a BuildOrder
|
||||
|
||||
Allocated stock can be unallocated with a number of filters:
|
||||
|
||||
- output: Filter against a particular build output (blank = untracked stock)
|
||||
- bom_item: Filter against a particular BOM line item
|
||||
|
||||
"""
|
||||
|
||||
bom_item = serializers.PrimaryKeyRelatedField(
|
||||
queryset=BomItem.objects.all(),
|
||||
many=False,
|
||||
allow_null=True,
|
||||
required=False,
|
||||
label=_('BOM Item'),
|
||||
)
|
||||
|
||||
output = serializers.PrimaryKeyRelatedField(
|
||||
queryset=StockItem.objects.filter(
|
||||
is_building=True,
|
||||
),
|
||||
many=False,
|
||||
allow_null=True,
|
||||
required=False,
|
||||
label=_("Build output"),
|
||||
)
|
||||
|
||||
def validate_output(self, stock_item):
|
||||
|
||||
# Stock item must point to the same build order!
|
||||
build = self.context['build']
|
||||
|
||||
if stock_item and stock_item.build != build:
|
||||
raise ValidationError(_("Build output must point to the same build"))
|
||||
|
||||
return stock_item
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
'Save' the serializer data.
|
||||
This performs the actual unallocation against the build order
|
||||
"""
|
||||
|
||||
build = self.context['build']
|
||||
|
||||
data = self.validated_data
|
||||
|
||||
build.unallocateStock(
|
||||
bom_item=data['bom_item'],
|
||||
output=data['output']
|
||||
)
|
||||
|
||||
|
||||
class BuildAllocationItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
A serializer for allocating a single stock item against a build order
|
||||
|
@ -462,12 +462,9 @@ $("#btn-auto-allocate").on('click', function() {
|
||||
});
|
||||
|
||||
$('#btn-unallocate').on('click', function() {
|
||||
launchModalForm(
|
||||
"{% url 'build-unallocate' build.id %}",
|
||||
{
|
||||
success: reloadTable,
|
||||
}
|
||||
);
|
||||
unallocateStock({{ build.id }}, {
|
||||
table: '#allocation-table-untracked',
|
||||
});
|
||||
});
|
||||
|
||||
$('#allocate-selected-items').click(function() {
|
||||
|
@ -1,15 +0,0 @@
|
||||
{% extends "modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
{% block pre_form_content %}
|
||||
|
||||
{{ block.super }}
|
||||
|
||||
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Are you sure you wish to unallocate all stock for this build?" %}
|
||||
<br>
|
||||
{% trans "All incomplete stock allocations will be removed from the build" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -323,22 +323,3 @@ class TestBuildViews(TestCase):
|
||||
|
||||
b = Build.objects.get(pk=1)
|
||||
self.assertEqual(b.status, 30) # Build status is now CANCELLED
|
||||
|
||||
def test_build_unallocate(self):
|
||||
""" Test the build unallocation view (ajax form) """
|
||||
|
||||
url = reverse('build-unallocate', args=(1,))
|
||||
|
||||
# Test without confirmation
|
||||
response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = json.loads(response.content)
|
||||
self.assertFalse(data['form_valid'])
|
||||
|
||||
# Test with confirmation
|
||||
response = self.client.post(url, {'confirm': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = json.loads(response.content)
|
||||
self.assertTrue(data['form_valid'])
|
||||
|
@ -12,7 +12,6 @@ build_detail_urls = [
|
||||
url(r'^create-output/', views.BuildOutputCreate.as_view(), name='build-output-create'),
|
||||
url(r'^delete-output/', views.BuildOutputDelete.as_view(), name='build-output-delete'),
|
||||
url(r'^complete-output/', views.BuildOutputComplete.as_view(), name='build-output-complete'),
|
||||
url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'),
|
||||
url(r'^complete/', views.BuildComplete.as_view(), name='build-complete'),
|
||||
|
||||
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
|
||||
|
@ -10,14 +10,13 @@ from django.core.exceptions import ValidationError
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.forms import HiddenInput
|
||||
|
||||
from part.models import Part
|
||||
from .models import Build
|
||||
from . import forms
|
||||
from stock.models import StockLocation, StockItem
|
||||
|
||||
from InvenTree.views import AjaxUpdateView, AjaxDeleteView
|
||||
from InvenTree.views import InvenTreeRoleMixin
|
||||
from InvenTree.helpers import str2bool, extract_serial_numbers, isNull
|
||||
from InvenTree.helpers import str2bool, extract_serial_numbers
|
||||
from InvenTree.status_codes import BuildStatus, StockStatus
|
||||
|
||||
|
||||
@ -246,88 +245,6 @@ class BuildOutputDelete(AjaxUpdateView):
|
||||
}
|
||||
|
||||
|
||||
class BuildUnallocate(AjaxUpdateView):
|
||||
""" View to un-allocate all parts from a build.
|
||||
|
||||
Provides a simple confirmation dialog with a BooleanField checkbox.
|
||||
"""
|
||||
|
||||
model = Build
|
||||
form_class = forms.UnallocateBuildForm
|
||||
ajax_form_title = _("Unallocate Stock")
|
||||
ajax_template_name = "build/unallocate.html"
|
||||
|
||||
def get_initial(self):
|
||||
|
||||
initials = super().get_initial()
|
||||
|
||||
# Pointing to a particular build output?
|
||||
output = self.get_param('output')
|
||||
|
||||
if output:
|
||||
initials['output_id'] = output
|
||||
|
||||
# Pointing to a particular part?
|
||||
part = self.get_param('part')
|
||||
|
||||
if part:
|
||||
initials['part_id'] = part
|
||||
|
||||
return initials
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
build = self.get_object()
|
||||
form = self.get_form()
|
||||
|
||||
confirm = request.POST.get('confirm', False)
|
||||
|
||||
output_id = request.POST.get('output_id', 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)
|
||||
|
||||
try:
|
||||
part = Part.objects.get(pk=part_id)
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
part = None
|
||||
|
||||
valid = False
|
||||
|
||||
if confirm is False:
|
||||
form.add_error('confirm', _('Confirm unallocation of build stock'))
|
||||
form.add_error(None, _('Check the confirmation box'))
|
||||
else:
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
return self.renderJsonResponse(request, form, data)
|
||||
|
||||
|
||||
class BuildComplete(AjaxUpdateView):
|
||||
"""
|
||||
View to mark the build as complete.
|
||||
|
@ -208,15 +208,10 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
|
||||
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(
|
||||
`/build/${buildId}/unallocate/`,
|
||||
{
|
||||
success: reloadTable,
|
||||
data: {
|
||||
output: pk,
|
||||
}
|
||||
}
|
||||
);
|
||||
unallocateStock(buildId, {
|
||||
output: pk,
|
||||
table: table,
|
||||
});
|
||||
});
|
||||
|
||||
$(panel).find(`#button-output-delete-${outputId}`).click(function() {
|
||||
@ -236,6 +231,49 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Unallocate stock against a particular build order
|
||||
*
|
||||
* Options:
|
||||
* - output: pk value for a stock item "build output"
|
||||
* - bom_item: pk value for a particular BOMItem (build item)
|
||||
*/
|
||||
function unallocateStock(build_id, options={}) {
|
||||
|
||||
var url = `/api/build/${build_id}/unallocate/`;
|
||||
|
||||
var html = `
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "Are you sure you wish to unallocate stock items from this build?" %}
|
||||
</dvi>
|
||||
`;
|
||||
|
||||
constructForm(url, {
|
||||
method: 'POST',
|
||||
confirm: true,
|
||||
preFormContent: html,
|
||||
fields: {
|
||||
output: {
|
||||
hidden: true,
|
||||
value: options.output,
|
||||
},
|
||||
bom_item: {
|
||||
hidden: true,
|
||||
value: options.bom_item,
|
||||
},
|
||||
},
|
||||
title: '{% trans "Unallocate Stock Items" %}',
|
||||
onSuccess: function(response, opts) {
|
||||
if (options.table) {
|
||||
// Reload the parent table
|
||||
$(options.table).bootstrapTable('refresh');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
function loadBuildOrderAllocationTable(table, options={}) {
|
||||
/**
|
||||
* Load a table showing all the BuildOrder allocations for a given part
|
||||
@ -469,17 +507,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
|
||||
// Callback for 'unallocate' button
|
||||
$(table).find('.button-unallocate').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(`/build/${buildId}/unallocate/`,
|
||||
{
|
||||
success: reloadTable,
|
||||
data: {
|
||||
output: outputId,
|
||||
part: pk,
|
||||
}
|
||||
}
|
||||
);
|
||||
// Extract row data from the table
|
||||
var idx = $(this).closest('tr').attr('data-index');
|
||||
var row = $(table).bootstrapTable('getData')[idx];
|
||||
|
||||
unallocateStock(buildId, {
|
||||
bom_item: row.pk,
|
||||
output: outputId == 'untracked' ? null : outputId,
|
||||
table: table,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user