2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +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:
Oliver 2021-10-14 10:32:43 +11:00
parent 1cbce5dfbf
commit 7dfffcd5d3
10 changed files with 161 additions and 201 deletions

View File

@ -21,7 +21,7 @@ from InvenTree.status_codes import BuildStatus
from .models import Build, BuildItem, BuildOrderAttachment from .models import Build, BuildItem, BuildOrderAttachment
from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer
from .serializers import BuildAllocationSerializer from .serializers import BuildAllocationSerializer, BuildUnallocationSerializer
class BuildFilter(rest_filters.FilterSet): class BuildFilter(rest_filters.FilterSet):
@ -184,6 +184,42 @@ class BuildDetail(generics.RetrieveUpdateAPIView):
serializer_class = BuildSerializer 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): class BuildAllocate(generics.CreateAPIView):
""" """
API endpoint to allocate stock items to a build order API endpoint to allocate stock items to a build order
@ -349,6 +385,7 @@ build_api_urls = [
# Build Detail # Build Detail
url(r'^(?P<pk>\d+)/', include([ url(r'^(?P<pk>\d+)/', include([
url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'), 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'), url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'),
])), ])),

View File

@ -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): class CompleteBuildForm(HelperForm):
""" """
Form for marking a build as complete Form for marking a build as complete

View File

@ -587,9 +587,13 @@ class Build(MPTTModel):
self.save() self.save()
@transaction.atomic @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( allocations = BuildItem.objects.filter(
@ -597,34 +601,8 @@ class Build(MPTTModel):
install_into=output install_into=output
) )
if part: if bom_item:
allocations = allocations.filter(stock_item__part=part) allocations = allocations.filter(bom_item=bom_item)
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()

View File

@ -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): class BuildAllocationItemSerializer(serializers.Serializer):
""" """
A serializer for allocating a single stock item against a build order A serializer for allocating a single stock item against a build order

View File

@ -462,12 +462,9 @@ $("#btn-auto-allocate").on('click', function() {
}); });
$('#btn-unallocate').on('click', function() { $('#btn-unallocate').on('click', function() {
launchModalForm( unallocateStock({{ build.id }}, {
"{% url 'build-unallocate' build.id %}", table: '#allocation-table-untracked',
{ });
success: reloadTable,
}
);
}); });
$('#allocate-selected-items').click(function() { $('#allocate-selected-items').click(function() {

View File

@ -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 %}

View File

@ -323,22 +323,3 @@ class TestBuildViews(TestCase):
b = Build.objects.get(pk=1) b = Build.objects.get(pk=1)
self.assertEqual(b.status, 30) # Build status is now CANCELLED 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'])

View File

@ -12,7 +12,6 @@ build_detail_urls = [
url(r'^create-output/', views.BuildOutputCreate.as_view(), name='build-output-create'), 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'^delete-output/', views.BuildOutputDelete.as_view(), name='build-output-delete'),
url(r'^complete-output/', views.BuildOutputComplete.as_view(), name='build-output-complete'), 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'^complete/', views.BuildComplete.as_view(), name='build-complete'),
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),

View File

@ -10,14 +10,13 @@ from django.core.exceptions import ValidationError
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.forms import HiddenInput from django.forms import HiddenInput
from part.models import Part
from .models import Build from .models import Build
from . import forms from . import forms
from stock.models import StockLocation, StockItem from stock.models import StockLocation, StockItem
from InvenTree.views import AjaxUpdateView, AjaxDeleteView from InvenTree.views import AjaxUpdateView, AjaxDeleteView
from InvenTree.views import InvenTreeRoleMixin 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 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): class BuildComplete(AjaxUpdateView):
""" """
View to mark the build as complete. View to mark the build as complete.

View File

@ -208,15 +208,10 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
var pk = $(this).attr('pk'); var pk = $(this).attr('pk');
launchModalForm( unallocateStock(buildId, {
`/build/${buildId}/unallocate/`,
{
success: reloadTable,
data: {
output: pk, output: pk,
} table: table,
} });
);
}); });
$(panel).find(`#button-output-delete-${outputId}`).click(function() { $(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={}) { function loadBuildOrderAllocationTable(table, options={}) {
/** /**
* Load a table showing all the BuildOrder allocations for a given part * 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 // Callback for 'unallocate' button
$(table).find('.button-unallocate').click(function() { $(table).find('.button-unallocate').click(function() {
var pk = $(this).attr('pk');
launchModalForm(`/build/${buildId}/unallocate/`, // Extract row data from the table
{ var idx = $(this).closest('tr').attr('data-index');
success: reloadTable, var row = $(table).bootstrapTable('getData')[idx];
data: {
output: outputId, unallocateStock(buildId, {
part: pk, bom_item: row.pk,
} output: outputId == 'untracked' ? null : outputId,
} table: table,
); });
}); });
} }