mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +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:
		@@ -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: {
 | 
			
		||||
        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,
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user