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:
parent
1cbce5dfbf
commit
7dfffcd5d3
@ -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'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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)
|
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'])
|
|
||||||
|
@ -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'),
|
||||||
|
@ -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.
|
||||||
|
@ -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/`,
|
output: pk,
|
||||||
{
|
table: table,
|
||||||
success: reloadTable,
|
});
|
||||||
data: {
|
|
||||||
output: pk,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$(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,
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user