From 327381357b2ab6d7ef5aaf0cfebc9b0572964979 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 18 May 2023 14:04:57 +1000 Subject: [PATCH] Adds ability to partially scrap build outputs (#4846) * BuildOrder updates: - Use batch code generation when creating a new build output - Allow partial scrapping of build outputs * Fixes for stock table * Bump API version * Update unit tests --- InvenTree/InvenTree/api_version.py | 5 ++- InvenTree/build/models.py | 17 +++++++- InvenTree/build/serializers.py | 46 ++++++++++++++++++++- InvenTree/build/templates/build/detail.html | 4 +- InvenTree/build/test_api.py | 3 ++ InvenTree/stock/models.py | 10 +++-- InvenTree/templates/filter_list.html | 2 +- InvenTree/templates/js/translated/build.js | 31 +++++++++++++- InvenTree/templates/stock_table.html | 2 +- 9 files changed, 107 insertions(+), 13 deletions(-) diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index 627433fe95..8e8855a837 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -2,11 +2,14 @@ # InvenTree API version -INVENTREE_API_VERSION = 113 +INVENTREE_API_VERSION = 115 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v115 - > 2023-05-18 : https://github.com/inventree/InvenTree/pull/4846 + - Adds ability to partially scrap a build output + v114 -> 2023-05-16 : https://github.com/inventree/InvenTree/pull/4825 - Adds "delivery_date" to shipments diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 5e32ab1f1b..ee9c01fcf3 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -797,7 +797,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models. items.all().delete() @transaction.atomic - def scrap_build_output(self, output, location, **kwargs): + def scrap_build_output(self, output, quantity, location, **kwargs): """Mark a particular build output as scrapped / rejected - Mark the output as "complete" @@ -809,10 +809,25 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models. if not output: raise ValidationError(_("No build output specified")) + if quantity <= 0: + raise ValidationError({ + 'quantity': _("Quantity must be greater than zero") + }) + + if quantity > output.quantity: + raise ValidationError({ + 'quantity': _("Quantity cannot be greater than the output quantity") + }) + user = kwargs.get('user', None) notes = kwargs.get('notes', '') discard_allocations = kwargs.get('discard_allocations', False) + if quantity < output.quantity: + # Split output into two items + output = output.splitStock(quantity, location=location, user=user) + output.build = self + # Update build output item output.is_building = False output.status = StockStatus.REJECTED diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index bba5f75757..e364a1eba8 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -17,7 +17,7 @@ import InvenTree.helpers from InvenTree.serializers import InvenTreeDecimalField from InvenTree.status_codes import StockStatus -from stock.models import StockItem, StockLocation +from stock.models import generate_batch_code, StockItem, StockLocation from stock.serializers import StockItemSerializerBrief, LocationSerializer from part.models import BomItem @@ -181,6 +181,45 @@ class BuildOutputSerializer(serializers.Serializer): return output +class BuildOutputQuantitySerializer(BuildOutputSerializer): + """Serializer for a single build output, with additional quantity field""" + + class Meta: + """Serializer metaclass""" + fields = BuildOutputSerializer.Meta.fields + [ + 'quantity', + ] + + quantity = serializers.DecimalField( + max_digits=15, + decimal_places=5, + min_value=0, + required=True, + label=_('Quantity'), + help_text=_('Enter quantity for build output'), + ) + + def validate(self, data): + """Validate the serializer data""" + + data = super().validate(data) + + output = data.get('output') + quantity = data.get('quantity') + + if quantity <= 0: + raise ValidationError({ + 'quantity': _('Quantity must be greater than zero') + }) + + if quantity > output.quantity: + raise ValidationError({ + 'quantity': _("Quantity cannot be greater than the output quantity") + }) + + return data + + class BuildOutputCreateSerializer(serializers.Serializer): """Serializer for creating a new BuildOutput against a BuildOrder. @@ -226,6 +265,7 @@ class BuildOutputCreateSerializer(serializers.Serializer): batch_code = serializers.CharField( required=False, allow_blank=True, + default=generate_batch_code, label=_('Batch Code'), help_text=_('Batch code for this build output'), ) @@ -362,7 +402,7 @@ class BuildOutputScrapSerializer(serializers.Serializer): 'notes', ] - outputs = BuildOutputSerializer( + outputs = BuildOutputQuantitySerializer( many=True, required=True, ) @@ -412,8 +452,10 @@ class BuildOutputScrapSerializer(serializers.Serializer): with transaction.atomic(): for item in outputs: output = item['output'] + quantity = item['quantity'] build.scrap_build_output( output, + quantity, data.get('location', None), user=request.user, notes=data.get('notes', ''), diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 162263783a..95ae46c8eb 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -302,7 +302,7 @@
- {% include "stock_table.html" with read_only=True prefix="build-" %} + {% include "stock_table.html" with prefix="build-" %}
@@ -343,6 +343,7 @@ onPanelLoad('consumed', function() { loadStockTable($('#consumed-stock-table'), { + filterTarget: '#filter-list-consumed-stock', params: { location_detail: true, part_detail: true, @@ -354,6 +355,7 @@ onPanelLoad('consumed', function() { onPanelLoad('completed', function() { loadStockTable($("#build-stock-table"), { + filterTarget: '#filter-list-build-stock', params: { location_detail: true, part_detail: true, diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index d5bad81523..622571289f 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -1027,12 +1027,15 @@ class BuildOutputScrapTest(BuildAPITest): 'outputs': [ { 'output': outputs[0].pk, + 'quantity': outputs[0].quantity, }, { 'output': outputs[1].pk, + 'quantity': outputs[1].quantity, }, { 'output': outputs[2].pk, + 'quantity': outputs[2].quantity, }, ], 'location': 1, diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 074265ed41..5055ccd6ec 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -1575,7 +1575,7 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo self.save() @transaction.atomic - def splitStock(self, quantity, location, user, **kwargs): + def splitStock(self, quantity, location=None, user=None, **kwargs): """Split this stock item into two items, in the same location. Stock tracking notes for this StockItem will be duplicated, @@ -1585,9 +1585,11 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo quantity: Number of stock items to remove from this entity, and pass to the next location: Where to move the new StockItem to - Notes: - The provided quantity will be subtracted from this item and given to the new one. - The new item will have a different StockItem ID, while this will remain the same. + Returns: + The new StockItem object + + - The provided quantity will be subtracted from this item and given to the new one. + - The new item will have a different StockItem ID, while this will remain the same. """ notes = kwargs.get('notes', '') diff --git a/InvenTree/templates/filter_list.html b/InvenTree/templates/filter_list.html index 73fa03ccb0..eebea0ed0d 100644 --- a/InvenTree/templates/filter_list.html +++ b/InvenTree/templates/filter_list.html @@ -1 +1 @@ -
+
diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 0d675bb582..49c620e18b 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -463,7 +463,7 @@ function unallocateStock(build_id, options={}) { /* * Helper function to render a single build output in a modal form */ -function renderBuildOutput(output, opts={}) { +function renderBuildOutput(output, options={}) { let pk = output.pk; let output_html = imageHoverIcon(output.part_detail.thumbnail); @@ -494,10 +494,31 @@ function renderBuildOutput(output, opts={}) { } ); + let quantity_field = ''; + + if (options.adjust_quantity) { + quantity_field = constructField( + `outputs_quantity_${pk}`, + { + type: 'decimal', + value: output.quantity, + min_value: 0, + max_value: output.quantity, + required: true, + }, + { + hideLabels: true, + } + ); + + quantity_field = `${quantity_field}`; + } + let html = ` ${field} ${output.part_detail.full_name} + ${quantity_field} ${buttons} `; @@ -645,7 +666,9 @@ function scrapBuildOutputs(build_id, outputs, options={}) { let table_entries = ''; outputs.forEach(function(output) { - table_entries += renderBuildOutput(output); + table_entries += renderBuildOutput(output, { + adjust_quantity: true, + }); }); var html = ` @@ -660,6 +683,7 @@ function scrapBuildOutputs(build_id, outputs, options={}) { + @@ -701,11 +725,14 @@ function scrapBuildOutputs(build_id, outputs, options={}) { outputs.forEach(function(output) { let pk = output.pk; let row = $(opts.modal).find(`#output_row_${pk}`); + let quantity = getFormFieldValue(`outputs_quantity_${pk}`, {}, opts); if (row.exists()) { data.outputs.push({ output: pk, + quantity: quantity, }); + output_pk_values.push(pk); } }); diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html index 26a7a5abea..fdbb284f8d 100644 --- a/InvenTree/templates/stock_table.html +++ b/InvenTree/templates/stock_table.html @@ -45,7 +45,7 @@ {% endif %} {% endif %} - {% include "filter_list.html" with id="stock" %} + {% include "filter_list.html" with prefix=prefix id="stock" %}
{% trans "Output" %}{% trans "Quantity" %}