From f90a27d01dfa7fcd1d5e49016e7022ac538e3348 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 15 Feb 2022 12:51:48 +1100 Subject: [PATCH] Adds a new API endpoint for creating build outputs --- InvenTree/build/api.py | 24 ++++ InvenTree/build/serializers.py | 117 ++++++++++++++++++++ InvenTree/build/templates/build/detail.html | 9 ++ InvenTree/templates/js/translated/build.js | 63 +++++++++++ 4 files changed, 213 insertions(+) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 57ffe88cf3..310d4d7f09 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -232,6 +232,29 @@ class BuildUnallocate(generics.CreateAPIView): return ctx +class BuildOutputCreate(generics.CreateAPIView): + """ + API endpoint for creating new build output(s) + """ + + queryset = Build.objects.none() + + serializer_class = build.serializers.BuildOutputCreateSerializer + + def get_serializer_context(self): + ctx = super().get_serializer_context() + + ctx['request'] = self.request + ctx['to_complete'] = True + + try: + ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None)) + except: + pass + + return ctx + + class BuildOutputComplete(generics.CreateAPIView): """ API endpoint for completing build outputs @@ -455,6 +478,7 @@ build_api_urls = [ url(r'^(?P\d+)/', include([ url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'), url(r'^complete/', BuildOutputComplete.as_view(), name='api-build-output-complete'), + url(r'^create-output/', BuildOutputCreate.as_view(), name='api-build-output-create'), url(r'^delete-outputs/', BuildOutputDelete.as_view(), name='api-build-output-delete'), url(r'^finish/', BuildFinish.as_view(), name='api-build-finish'), url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'), diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index bc9d018cbe..e44a00c306 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -19,6 +19,7 @@ from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentS from InvenTree.serializers import UserSerializerBrief, ReferenceIndexingSerializerMixin import InvenTree.helpers +from InvenTree.helpers import extract_serial_numbers from InvenTree.serializers import InvenTreeDecimalField from InvenTree.status_codes import StockStatus @@ -170,6 +171,122 @@ class BuildOutputSerializer(serializers.Serializer): ] +class BuildOutputCreateSerializer(serializers.Serializer): + """ + Serializer for creating a new BuildOutput against a BuildOrder. + + URL pattern is "/api/build//create-output/", where is the PK of a Build. + + The Build object is provided to the serializer context. + """ + + quantity = serializers.DecimalField( + max_digits=15, + decimal_places=5, + min_value=0, + required=True, + label=_('Quantity'), + help_text=_('Enter quantity for build output'), + ) + + def get_build(self): + return self.context["build"] + + def get_part(self): + return self.get_build().part + + def validate_quantity(self, quantity): + + if quantity < 0: + raise ValidationError(_("Quantity must be greater than zero")) + + part = self.get_part() + + if int(quantity) != quantity: + # Quantity must be an integer value if the part being built is trackable + if part.trackable: + raise ValidationError(_("Integer quantity required for trackable parts")) + + if part.has_trackable_parts(): + raise ValidationError(_("Integer quantity required, as the bill of materials contains tracakble parts")) + + return quantity + + batch_code = serializers.CharField( + required=False, + allow_blank=True, + label=_('Batch Code'), + help_text=_('Batch code for this build output'), + ) + + serial_numbers = serializers.CharField( + allow_blank=True, + required=False, + label=_('Serial Numbers'), + help_text=_('Enter serial numbers for build outputs'), + ) + + def validate_serial_numbers(self, serial_numbers): + + serial_numbers = serial_numbers.strip() + + # TODO: Field level validation necessary here? + return serial_numbers + + auto_allocate = serializers.BooleanField( + required=False, + default=False, + label=_('Auto Allocate Serial Numbers'), + help_text=_('Automatically allocate required items with matching serial numbers'), + ) + + def validate(self, data): + """ + Perform form validation + """ + + build = self.get_build() + part = self.get_part() + serials = None + + quantity = data['quantity'] + serial_numbers = data.get('serial_numbers', '') + + if serial_numbers: + + try: + serials = extract_serial_numbers(serial_numbers, quantity, part.getLatestSerialNumberInt()) + except DjangoValidationError as e: + raise ValidationError({ + 'serial_numbers': e.messages, + }) + + # Check for conflicting serial numbesr + existing = [] + + for serial in serials: + if part.checkIfSerialNumberExists(serial): + existing.append(serial) + + if len(existing) > 0: + + msg = _("The following serial numbers already exist") + msg += " : " + msg += ",".join([str(e) for e in existing]) + + raise ValidationError({ + 'serial_numbers': msg, + }) + + return data + + def save(self): + """ + Generate the new build output(s) + """ + ... + + class BuildOutputDeleteSerializer(serializers.Serializer): """ DRF serializer for deleting (cancelling) one or more build outputs diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index ff335d139c..484346f89e 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -321,6 +321,15 @@ {{ block.super }} $('#btn-create-output').click(function() { + + createBuildOutput( + {{ build.pk }}, + { + trackable_parts: {% if build.part.has_trackable_parts %}true{% else %}false{% endif%}, + } + ); + + return; launchModalForm('{% url "build-output-create" build.id %}', { reload: true, diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 5782218780..bb6b3c9daf 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -21,6 +21,7 @@ /* exported allocateStockToBuild, completeBuildOrder, + createBuildOutput, editBuildOrder, loadAllocationTable, loadBuildOrderAllocationTable, @@ -175,6 +176,68 @@ function completeBuildOrder(build_id, options={}) { } +/* + * Construct a new build output against the provided build + */ +function createBuildOutput(build_id, options) { + + // Request build order information from the server + inventreeGet( + `/api/build/${build_id}/`, + {}, + { + success: function(build) { + + var html = ''; + + var trackable = build.part_detail.trackable; + var remaining = Math.max(0, build.quantity - build.completed); + + var fields = { + quantity: { + value: remaining, + }, + serial_numbers: { + hidden: !trackable, + required: options.trackable_parts || trackable, + }, + batch_code: {}, + auto_allocate: {}, + }; + + if (options.trackable_parts) { + html += ` +
+ {% trans "The Bill of Materials contains trackable parts" %}.
+ {% trans "Build outputs must be generated individually" %}. +
+ `; + } + + if (trackable) { + html += ` +
+ {% trans "Trackable parts can have serial numbers specified" %}
+ {% trans "Enter serial numbers to generate multiple single build outputs" %} +
+ `; + } + + constructForm(`/api/build/${build_id}/create-output/`, { + method: 'POST', + title: '{% trans "Create Build Output" %}', + confirm: true, + fields: fields, + preFormContent: html, + }); + + } + } + ); + +} + + /* * Construct a set of output buttons for a particular build output */