From 54dd05a24d46d90169f568e3b6162606dcf2133c Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 23:13:01 +1100 Subject: [PATCH 01/26] Add an API serializer to complete build outputs --- InvenTree/build/api.py | 57 ++++----- InvenTree/build/models.py | 7 +- InvenTree/build/serializers.py | 117 +++++++++++++++++- .../build/templates/build/build_base.html | 10 +- InvenTree/build/test_build.py | 4 +- InvenTree/build/views.py | 2 +- InvenTree/order/api.py | 17 +-- InvenTree/templates/js/translated/build.js | 2 +- 8 files changed, 153 insertions(+), 63 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 7920003d8b..ddedd60ce6 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -6,7 +6,7 @@ JSON API for the Build app from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ - +from django.shortcuts import get_object_or_404 from django.conf.urls import url, include from rest_framework import filters, generics @@ -20,7 +20,7 @@ from InvenTree.helpers import str2bool, isNull from InvenTree.status_codes import BuildStatus from .models import Build, BuildItem, BuildOrderAttachment -from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer +from .serializers import BuildAttachmentSerializer, BuildCompleteSerializer, BuildSerializer, BuildItemSerializer from .serializers import BuildAllocationSerializer, BuildUnallocationSerializer @@ -196,30 +196,34 @@ class BuildUnallocate(generics.CreateAPIView): 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['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None)) ctx['request'] = self.request return ctx +class BuildComplete(generics.CreateAPIView): + """ + API endpoint for completing build outputs + """ + + queryset = Build.objects.none() + + serializer_class = BuildCompleteSerializer + + def get_serializer_context(self): + ctx = super().get_serializer_context() + + ctx['request'] = self.request + ctx['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None)) + + return ctx + + class BuildAllocate(generics.CreateAPIView): """ API endpoint to allocate stock items to a build order @@ -236,20 +240,6 @@ class BuildAllocate(generics.CreateAPIView): serializer_class = BuildAllocationSerializer - 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 (Build.DoesNotExist, ValueError): - raise ValidationError(_("Matching build order does not exist")) - - return build - def get_serializer_context(self): """ Provide the Build object to the serializer context @@ -257,7 +247,7 @@ class BuildAllocate(generics.CreateAPIView): context = super().get_serializer_context() - context['build'] = self.get_build() + context['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None)) context['request'] = self.request return context @@ -385,6 +375,7 @@ build_api_urls = [ # Build Detail url(r'^(?P\d+)/', include([ url(r'^allocate/', BuildAllocate.as_view(), name='api-build-allocate'), + url(r'^complete/', BuildComplete.as_view(), name='api-build-complete'), url(r'^unallocate/', BuildUnallocate.as_view(), name='api-build-unallocate'), url(r'^.*$', BuildDetail.as_view(), name='api-build-detail'), ])), diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 449776579e..994d060585 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -722,7 +722,7 @@ class Build(MPTTModel): items.all().delete() @transaction.atomic - def completeBuildOutput(self, output, user, **kwargs): + def complete_build_output(self, output, user, **kwargs): """ Complete a particular build output @@ -739,10 +739,6 @@ class Build(MPTTModel): allocated_items = output.items_to_install.all() for build_item in allocated_items: - - # TODO: This is VERY SLOW as each deletion from the database takes ~1 second to complete - # TODO: Use the background worker process to handle this task! - # Complete the allocation of stock for that item build_item.complete_allocation(user) @@ -768,6 +764,7 @@ class Build(MPTTModel): # Increase the completed quantity for this build self.completed += output.quantity + self.save() def requiredQuantity(self, part, output): diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 547f565905..cf7c8065fe 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -18,9 +18,10 @@ from rest_framework.serializers import ValidationError from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief +from InvenTree.status_codes import StockStatus import InvenTree.helpers -from stock.models import StockItem +from stock.models import StockItem, StockLocation from stock.serializers import StockItemSerializerBrief, LocationSerializer from part.models import BomItem @@ -120,6 +121,120 @@ class BuildSerializer(InvenTreeModelSerializer): ] +class BuildOutputSerializer(serializers.Serializer): + """ + Serializer for a "BuildOutput" + + Note that a "BuildOutput" is really just a StockItem which is "in production"! + """ + + output = serializers.PrimaryKeyRelatedField( + queryset=StockItem.objects.all(), + many=False, + allow_null=False, + required=True, + label=_('Build Output'), + ) + + def validate_output(self, output): + + build = self.context['build'] + + # The stock item must point to the build + if output.build != build: + raise ValidationError(_("Build output does not match the parent build")) + + # The part must match! + if output.part != build.part: + raise ValidationError(_("Output part does not match BuildOrder part")) + + # The build output must be "in production" + if not output.is_building: + raise ValidationError(_("This build output has already been completed")) + + return output + + class Meta: + fields = [ + 'output', + ] + + +class BuildCompleteSerializer(serializers.Serializer): + """ + DRF serializer for completing one or more build outputs + """ + + class Meta: + fields = [ + 'outputs', + 'location', + 'status', + 'notes', + ] + + outputs = BuildOutputSerializer( + many=True, + required=True, + ) + + location = serializers.PrimaryKeyRelatedField( + queryset=StockLocation.objects.all(), + required=True, + many=False, + label=_("Location"), + help_text=_("Location for completed build outputs"), + ) + + status = serializers.ChoiceField( + choices=list(StockStatus.items()), + default=StockStatus.OK, + label=_("Status"), + ) + + notes = serializers.CharField( + label=_("Notes"), + required=False, + allow_blank=True, + ) + + def validate(self, data): + + super().validate(data) + + outputs = data.get('outputs', []) + + if len(outputs) == 0: + raise ValidationError(_("A list of build outputs must be provided")) + + return data + + def save(self): + """ + "save" the serializer to complete the build outputs + """ + + build = self.context['build'] + request = self.context['request'] + + data = self.validated_data + + outputs = data.get('outputs', []) + + # Mark the specified build outputs as "complete" + with transaction.atomic(): + for item in outputs: + + output = item['output'] + + build.complete_build_output( + output, + request.user, + status=data['status'], + notes=data.get('notes', '') + ) + + class BuildUnallocationSerializer(serializers.Serializer): """ DRF serializer for unallocating stock from a BuildOrder diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index e3119e6fdb..5f53fa36bb 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -96,11 +96,6 @@ src="{% static 'img/blank_image.png' %}" {% if roles.build.change %} - {% if build.active %} - - {% endif %}
+ {% if build.active %} + + {% endif %} {% endif %} {% endblock %} diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index a0874d0979..31e4cf1822 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -319,11 +319,11 @@ class BuildTest(TestCase): self.assertTrue(self.build.isFullyAllocated(self.output_1)) self.assertTrue(self.build.isFullyAllocated(self.output_2)) - self.build.completeBuildOutput(self.output_1, None) + self.build.complete_build_output(self.output_1, None) self.assertFalse(self.build.can_complete) - self.build.completeBuildOutput(self.output_2, None) + self.build.complete_build_output(self.output_2, None) self.assertTrue(self.build.can_complete) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 8c63c1296c..37ec567f1a 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -434,7 +434,7 @@ class BuildOutputComplete(AjaxUpdateView): stock_status = StockStatus.OK # Complete the build output - build.completeBuildOutput( + build.complete_build_output( output, self.request.user, location=location, diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index af30a3a5c5..0a540c3412 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ from django.conf.urls import url, include from django.db.models import Q, F +from django.shortcuts import get_object_or_404 from django_filters import rest_framework as rest_filters from rest_framework import generics @@ -232,25 +233,11 @@ class POReceive(generics.CreateAPIView): context = super().get_serializer_context() # Pass the purchase order through to the serializer for validation - context['order'] = self.get_order() + context['order'] = get_object_or_404(PurchaseOrder, pk=self.kwargs.get('pk', None)) context['request'] = self.request return context - def get_order(self): - """ - Returns the PurchaseOrder associated with this API endpoint - """ - - pk = self.kwargs.get('pk', None) - - try: - order = PurchaseOrder.objects.get(pk=pk) - except (PurchaseOrder.DoesNotExist, ValueError): - raise ValidationError(_("Matching purchase order does not exist")) - - return order - class POLineItemFilter(rest_filters.FilterSet): """ diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index d3499deedf..686e8e1bfa 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -151,7 +151,7 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) { // Add a button to "complete" the particular build output html += makeIconButton( - 'fa-check icon-green', 'button-output-complete', outputId, + 'fa-check-circle icon-green', 'button-output-complete', outputId, '{% trans "Complete build output" %}', { // disabled: true From b0a3280c05cbe06e932b050053adb5c01e71e718 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 23:13:08 +1100 Subject: [PATCH 02/26] Add unit testing --- InvenTree/build/test_api.py | 143 ++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index 017f0126c5..a1068d3e60 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -7,6 +7,7 @@ from django.urls import reverse from part.models import Part from build.models import Build, BuildItem +from stock.models import StockItem from InvenTree.status_codes import BuildStatus from InvenTree.api_tester import InvenTreeAPITestCase @@ -37,6 +38,148 @@ class BuildAPITest(InvenTreeAPITestCase): super().setUp() +class BuildCompleteTest(BuildAPITest): + """ + Unit testing for the build complete API endpoint + """ + + def setUp(self): + + super().setUp() + + self.build = Build.objects.get(pk=1) + + self.url = reverse('api-build-complete', kwargs={'pk': self.build.pk}) + + def test_invalid(self): + """ + Test with invalid data + """ + + # Test with an invalid build ID + self.post( + reverse('api-build-complete', kwargs={'pk': 99999}), + {}, + expected_code=404 + ) + + data = self.post(self.url, {}, expected_code=400).data + + self.assertIn("This field is required", str(data['outputs'])) + self.assertIn("This field is required", str(data['location'])) + + # Test with an invalid location + data = self.post( + self.url, + { + "outputs": [], + "location": 999999, + }, + expected_code=400 + ).data + + self.assertIn( + "Invalid pk", + str(data["location"]) + ) + + data = self.post( + self.url, + { + "outputs": [], + "location": 1, + }, + expected_code=400 + ).data + + self.assertIn("A list of build outputs must be provided", str(data)) + + stock_item = StockItem.objects.create( + part=self.build.part, + quantity=100, + ) + + post_data = { + "outputs": [ + { + "output": stock_item.pk, + }, + ], + "location": 1, + } + + # Post with a stock item that does not match the build + data = self.post( + self.url, + post_data, + expected_code=400 + ).data + + self.assertIn( + "Build output does not match the parent build", + str(data["outputs"][0]) + ) + + # Now, ensure that the stock item *does* match the build + stock_item.build = self.build + stock_item.save() + + data = self.post( + self.url, + post_data, + expected_code=400, + ).data + + self.assertIn( + "This build output has already been completed", + str(data["outputs"][0]["output"]) + ) + + def test_complete(self): + """ + Test build order completion + """ + + # We start without any outputs assigned against the build + self.assertEqual(self.build.incomplete_outputs.count(), 0) + + # Create some more build outputs + for ii in range(10): + self.build.create_build_output(10) + + # Check that we are in a known state + self.assertEqual(self.build.incomplete_outputs.count(), 10) + self.assertEqual(self.build.incomplete_count, 100) + self.assertEqual(self.build.completed, 0) + + # We shall complete 4 of these outputs + outputs = self.build.incomplete_outputs[0:4] + + self.post( + self.url, + { + "outputs": [{"output": output.pk} for output in outputs], + "location": 1, + "status": 50, # Item requires attention + }, + expected_code=201 + ) + + # There now should be 6 incomplete build outputs remaining + self.assertEqual(self.build.incomplete_outputs.count(), 6) + + # And there should be 4 completed outputs + outputs = self.build.complete_outputs + self.assertEqual(outputs.count(), 4) + + for output in outputs: + self.assertFalse(output.is_building) + self.assertEqual(output.build, self.build) + + self.build.refresh_from_db() + self.assertEqual(self.build.completed, 40) + + class BuildAllocationTest(BuildAPITest): """ Unit tests for allocation of stock items against a build order. From be6f417dff44a527f825bf86928d33fa732da08c Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 23:13:12 +1100 Subject: [PATCH 03/26] Random button cleanup --- .../build/templates/build/build_base.html | 2 +- .../order/templates/order/order_base.html | 42 ++++++++++------- .../templates/order/sales_order_base.html | 45 +++++++++++-------- 3 files changed, 53 insertions(+), 36 deletions(-) diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 5f53fa36bb..d821b1743e 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -91,7 +91,7 @@ src="{% static 'img/blank_image.png' %}" diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 8b98755900..ca7e70c4e3 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -36,31 +36,39 @@ src="{% static 'img/blank_image.png' %}"

{{ order.description }}{% include "clip.html"%}

- - + + {% if roles.purchase_order.change %} - + +
+ + +
{% if order.status == PurchaseOrderStatus.PENDING and order.lines.count > 0 %} {% elif order.status == PurchaseOrderStatus.PLACED %} - {% endif %} - {% if order.can_cancel %} - {% endif %} {% endif %} diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 3fd34e42b9..f7595cc182 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -47,30 +47,39 @@ src="{% static 'img/blank_image.png' %}"

{{ order.description }}{% include "clip.html"%}

- - + + {% if roles.sales_order.change %} - + +
+ + + +
{% if order.status == SalesOrderStatus.PENDING %} - {% endif %} {% endif %} -
{% endblock %} From bf47b57c82d42a91ca5a760388b1ff357af461b0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 23:46:21 +1100 Subject: [PATCH 04/26] working on updating build output tables --- InvenTree/build/api.py | 2 - InvenTree/build/templates/build/detail.html | 29 ++-- InvenTree/build/test_api.py | 2 +- InvenTree/order/api.py | 2 - InvenTree/templates/js/translated/build.js | 167 ++++++++++++++++++-- 5 files changed, 177 insertions(+), 25 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index ddedd60ce6..dc2e81b978 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -5,12 +5,10 @@ JSON API for the Build app # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 from django.conf.urls import url, include from rest_framework import filters, generics -from rest_framework.serializers import ValidationError from django_filters.rest_framework import DjangoFilterBackend from django_filters import rest_framework as rest_filters diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index cfba2046e3..d715149718 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -218,26 +218,21 @@

{% trans "Incomplete Build Outputs" %}

-
- {% if build.active %} + {% if build.active %} +
- {% endif %} +
- {% if build.incomplete_outputs %}
{% for item in build.incomplete_outputs %} {% include "build/allocation_card.html" with item=item tracked_items=build.has_tracked_bom_items %} {% endfor %}
- {% else %} -
- {% trans "Create a new build output" %}
- {% trans "No incomplete build outputs remain." %}
- {% trans "Create a new build output using the button above" %} -
+ +
{% endif %}
{% endif %} @@ -321,8 +316,22 @@ var buildInfo = { {% if build.take_from %} source_location: {{ build.take_from.pk }}, {% endif %} + {% if build.has_tracked_bom_items %} + tracked_parts: true, + {% else %} + tracked_parts: false, + {% endif %} }; +{% if build.active %} +loadBuildOutputTable( + buildInfo, + { + + } +); +{% endif %} + {% for item in build.incomplete_outputs %} // Get the build output as a javascript object inventreeGet('{% url 'api-stock-detail' item.pk %}', {}, diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index a1068d3e60..e14f52ee04 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -153,7 +153,7 @@ class BuildCompleteTest(BuildAPITest): self.assertEqual(self.build.completed, 0) # We shall complete 4 of these outputs - outputs = self.build.incomplete_outputs[0:4] + outputs = self.build.incomplete_outputs[0:4] self.post( self.url, diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 0a540c3412..42ed28dff0 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -5,7 +5,6 @@ JSON API for the Order app # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.utils.translation import ugettext_lazy as _ from django.conf.urls import url, include from django.db.models import Q, F from django.shortcuts import get_object_or_404 @@ -14,7 +13,6 @@ from django_filters import rest_framework as rest_filters from rest_framework import generics from rest_framework import filters, status from rest_framework.response import Response -from rest_framework.serializers import ValidationError from InvenTree.filters import InvenTreeOrderingFilter diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 686e8e1bfa..672c7013ac 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -108,10 +108,64 @@ function newBuildOrder(options={}) { } +/* + * Construct a set of output buttons for a particular build output + */ +function makeBuildOutputButtons(output_id, build_info, options={}) { + + var html = `
`; + + // Tracked parts? Must be individually allocated + if (build_info.tracked_parts) { + + // Add a button to allocate stock against this build output + html += makeIconButton( + 'fa-sign-in-alt icon-blue', + 'button-output-allocate', + output_id, + '{% trans "Allocate stock items to this build output" %}', + ); + + // Add a button to unallocate stock from this build output + html += makeIconButton( + 'fa-minus-circle icon-red', + 'build-output-unallocate', + output_id, + '{% trans "Unallocate stock from build output" %}', + ); + } + + // Add a button to "complete" this build output + html += makeIconButton( + 'fa-check-circle icon-green', + 'build-output-complete', + output_id, + '{% trans "Complete build output" %}', + ) + + // Add a button to "delete" this build output + html += makeIconButton( + 'fa-trash-alt icon-red', + 'button-output-delete', + output_id, + '{% trans "Delete build output" %}', + ); + + html += `
`; + + return html; + +} + + +// TODO "delete me" + function makeBuildOutputActionButtons(output, buildInfo, lines) { /* Generate action buttons for a build output. */ + var todo = "delete this function ok"; + var buildId = buildInfo.pk; var partId = buildInfo.part; @@ -357,17 +411,110 @@ function loadBuildOrderAllocationTable(table, options={}) { } +/* + * Display a "build output" table for a particular build. + * + * This displays a list of "active" (i.e. "in production") build outputs for a given build + * + */ +function loadBuildOutputTable(build_info, options={}) { + + var table = options.table || '#build-output-table'; + + var params = options.params || {}; + + // Mandatory query filters + params.part_detail = true; + params.is_building = true; + params.build = build_info.pk; + + var filters = {}; + + for (var key in params) { + filters[key] = params[key]; + } + + // TODO: Initialize filter list + + $(table).inventreeTable({ + url: '{% url "api-stock-list" %}', + queryParams: filters, + original: params, + showColumns: true, + name: 'build-outputs', + sortable: true, + search: true, + sidePagination: 'server', + formatNoMatches: function() { + return '{% trans "No active build outputs found" %}'; + }, + onPostBody: function() { + // TODO + }, + columns: [ + { + field: 'part', + title: '{% trans "Part" %}', + formatter: function(value, row) { + var thumb = row.part_detail.thumbnail; + + return imageHoverIcon(thumb) + row.part_detail.full_name + makePartIcons(row.part_detail); + } + }, + { + field: 'quantity', + title: '{% trans "Quantity" %}', + formatter: function(value, row) { + + var url = `/stock/item/${row.pk}/`; + + var text = ''; + + if (row.serial && row.quantity == 1) { + text = `{% trans "Serial Number" %}: ${row.serial}`; + } else { + text = `{% trans "Quantity" %}: ${row.quantity}`; + } + + return renderLink(text, url); + } + }, + { + field: 'allocated', + title: '{% trans "Allocated" %}', + formatter: function(value, row) { + return "TODO"; + } + }, + { + field: 'actions', + title: '', + formatter: function(value, row) { + return makeBuildOutputButtons( + row.pk, + build_info, + ); + } + } + ] + }); +} + + +/* + * Display the "allocation table" for a particular build output. + * + * This displays a table of required allocations for a particular build output + * + * Args: + * - buildId: The PK of the Build object + * - partId: The PK of the Part object + * - output: The StockItem object which is the "output" of the build + * - options: + * -- table: The #id of the table (will be auto-calculated if not provided) + */ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { - /* - * Load the "allocation table" for a particular build output. - * - * Args: - * - buildId: The PK of the Build object - * - partId: The PK of the Part object - * - output: The StockItem object which is the "output" of the build - * - options: - * -- table: The #id of the table (will be auto-calculated if not provided) - */ + var buildId = buildInfo.pk; var partId = buildInfo.part; From 0d49513092bad90e6c7247999211aa897e551d6d Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 23:58:03 +1100 Subject: [PATCH 05/26] Add button callbacks for top-level table --- InvenTree/templates/js/translated/build.js | 59 ++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 672c7013ac..4d8637d7ca 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -129,7 +129,7 @@ function makeBuildOutputButtons(output_id, build_info, options={}) { // Add a button to unallocate stock from this build output html += makeIconButton( 'fa-minus-circle icon-red', - 'build-output-unallocate', + 'button-output-unallocate', output_id, '{% trans "Unallocate stock from build output" %}', ); @@ -138,7 +138,7 @@ function makeBuildOutputButtons(output_id, build_info, options={}) { // Add a button to "complete" this build output html += makeIconButton( 'fa-check-circle icon-green', - 'build-output-complete', + 'button-output-complete', output_id, '{% trans "Complete build output" %}', ) @@ -436,6 +436,58 @@ function loadBuildOutputTable(build_info, options={}) { // TODO: Initialize filter list + function setupBuildOutputButtonCallbacks() { + + // Callback for the "allocate" button + $(table).find('.button-output-allocate').click(function() { + var pk = $(this).attr('pk'); + + // TODO + var todo = "Work out which stock items we need to allocate and launch the form"; + /* + allocateStockToBuild( + build_info.pk, + build_info.part, + + )*/ + }); + + // Callack for the "unallocate" button + $(table).find('.button-output-unallocate').click(function() { + var pk = $(this).attr('pk'); + + unallocateStock(build_info.pk, { + output: pk, + table: table + }); + }); + + // Callback for the "complete" button + $(table).find('.button-output-complete').click(function() { + var pk = $(this).attr('pk'); + + var todo = "write function to complete build output(s)"; + }); + + // Callback for the "delete" button + $(table).find('.button-output-delete').click(function() { + var pk = $(this).attr('pk'); + + // TODO: Move this to the API + launchModalForm( + `/build/${build_info.pk}/delete-output/`, + { + data: { + output: pk + }, + onSuccess: function() { + $(table).bootstrapTable('refresh'); + } + } + ); + }); + } + $(table).inventreeTable({ url: '{% url "api-stock-list" %}', queryParams: filters, @@ -449,7 +501,8 @@ function loadBuildOutputTable(build_info, options={}) { return '{% trans "No active build outputs found" %}'; }, onPostBody: function() { - // TODO + // Add callbacks for the buttons + setupBuildOutputButtonCallbacks(); }, columns: [ { From 4702c6b37f73161095476fb7a0a12f93f8835905 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Oct 2021 00:25:36 +1100 Subject: [PATCH 06/26] Refactorin' --- InvenTree/build/templates/build/detail.html | 75 +++++++++------- InvenTree/templates/js/translated/build.js | 94 +++++++++++++++++++++ 2 files changed, 137 insertions(+), 32 deletions(-) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index d715149718..b7cdf12921 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -308,40 +308,55 @@ loadStockTable($("#build-stock-table"), { url: "{% url 'api-stock-list' %}", }); -var buildInfo = { - pk: {{ build.pk }}, - quantity: {{ build.quantity }}, - completed: {{ build.completed }}, - part: {{ build.part.pk }}, - {% if build.take_from %} - source_location: {{ build.take_from.pk }}, - {% endif %} - {% if build.has_tracked_bom_items %} - tracked_parts: true, - {% else %} - tracked_parts: false, - {% endif %} -}; -{% if build.active %} -loadBuildOutputTable( - buildInfo, +// Get the list of BOM items required for this build +inventreeGet( + '{% url "api-bom-list" %}', { - - } -); -{% endif %} - -{% for item in build.incomplete_outputs %} -// Get the build output as a javascript object -inventreeGet('{% url 'api-stock-detail' item.pk %}', {}, + part: {{ build.part.pk }}, + sub_part_detail: true, + }, { success: function(response) { - loadBuildOutputAllocationTable(buildInfo, response); + + var build_info = { + pk: {{ build.pk }}, + part: {{ build.part.pk }}, + quantity: {{ build.quantity }}, + bom_items: response, + {% if build.take_from %} + source_location: {{ build.take_from.pk }}, + {% endif %} + {% if build.has_tracked_bom_items %} + tracked_parts: true, + {% else %} + tracked_parts: false, + {% endif %} + }; + + {% if build.active %} + loadBuildOutputTable(build_info); + {% endif %} + + {% for item in build.incomplete_outputs %} + // Get the build output as a javascript object + inventreeGet('{% url 'api-stock-detail' item.pk %}', {}, + { + success: function(response) { + loadBuildOutputAllocationTable(build_info, response); + } + } + ); + {% endfor %} + + {% if build.has_untracked_bom_items %} + // Load allocation table for un-tracked parts + loadBuildOutputAllocationTable(build_info, null); + {% endif %} + } } ); -{% endfor %} loadBuildTable($('#sub-build-table'), { url: '{% url "api-build-list" %}', @@ -351,6 +366,7 @@ loadBuildTable($('#sub-build-table'), { } }); + enableDragAndDrop( '#attachment-dropzone', '{% url "api-build-attachment-list" %}', @@ -425,11 +441,6 @@ $('#edit-notes').click(function() { }); }); -{% if build.has_untracked_bom_items %} -// Load allocation table for un-tracked parts -loadBuildOutputAllocationTable(buildInfo, null); -{% endif %} - function reloadTable() { $('#allocation-table-untracked').bootstrapTable('refresh'); } diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 4d8637d7ca..7c0185ec8b 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -428,6 +428,15 @@ function loadBuildOutputTable(build_info, options={}) { params.is_building = true; params.build = build_info.pk; + // Construct a list of "tracked" BOM items + var tracked_bom_items = []; + + build_info.bom_items.forEach(function(bom_item) { + if (bom_item.sub_part_detail.trackable) { + tracked_bom_items.push(bom_item); + }; + }); + var filters = {}; for (var key in params) { @@ -488,6 +497,83 @@ function loadBuildOutputTable(build_info, options={}) { }); } + /* + * Construct a "sub table" showing the required BOM items + */ + function constructBuildOutputSubTable(index, row, element) { + var sub_table_id = `output-sub-table-${row.pk}`; + + var html = ` +
+
+
+ `; + + element.html(html); + + var todo = "refactor the following fields, they are shared with the 'untracked' allocation table!"; + + $(`#${sub_table_id}`).bootstrapTable({ + data: tracked_bom_items, + showHeader: true, + columns: [ + { + field: 'part', + title: '{% trans "Required Part" %}', + formatter: function(value, row) { + var part = row.sub_part_detail; + + var url = `/part/${part.pk}/`; + var thumb = part.thumbnail || row.image; + var name = part.full_name; + + var html = imageHoverIcon(thumb) + renderLink(name, url) + makePartIcons(part); + + if (row.substitutes && row.substitutes.length > 0) { + html += makeIconBadge('fa-exchange-alt', '{% trans "Substitute parts available" %}'); + } + + if (row.allow_variants) { + html += makeIconBadge('fa-sitemap', '{% trans "Variant stock allowed" %}'); + } + + return html; + } + }, + { + field: 'reference', + title: '{% trans "Reference" %}', + sortable: true, + }, + { + field: 'quantity', + title: '{% trans "Quantity Per Item" %}', + sortable: true, + }, + { + field: 'allocated', + title: '{% trans "Allocated" %}', + formatter: function(value, row) { + return "todo"; + } + }, + { + field: 'actions', + title: '', + formatter: function(value, row) { + var html = `
`; + + html += "todo"; + + html += `
`; + + return html; + } + } + ] + }); + } + $(table).inventreeTable({ url: '{% url "api-stock-list" %}', queryParams: filters, @@ -497,6 +583,14 @@ function loadBuildOutputTable(build_info, options={}) { sortable: true, search: true, sidePagination: 'server', + detailView: build_info.tracked_parts || false, + detailViewByClick: true, + detailFilter: function(index, row) { + return true; + }, + detailFormatter: function(index, row, element) { + constructBuildOutputSubTable(index, row, element); + }, formatNoMatches: function() { return '{% trans "No active build outputs found" %}'; }, From 41e59e5311740b904288c297dda649947e0e6172 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 16 Oct 2021 13:01:19 +1100 Subject: [PATCH 07/26] Right align expand buttons --- InvenTree/templates/js/translated/build.js | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 7c0185ec8b..96594ed8dd 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -583,6 +583,7 @@ function loadBuildOutputTable(build_info, options={}) { sortable: true, search: true, sidePagination: 'server', + detailViewAlign: 'right', detailView: build_info.tracked_parts || false, detailViewByClick: true, detailFilter: function(index, row) { From b60296e494ef992e59aede431e1e62d452aeff59 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 16 Oct 2021 13:16:19 +1100 Subject: [PATCH 08/26] Add some more unit tests --- InvenTree/order/test_api.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 1f7905d1e3..899fa9a6fc 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -203,6 +203,23 @@ class PurchaseOrderTest(OrderTest): # And if we try to access the detail view again, it has gone response = self.get(url, expected_code=404) + def test_po_create(self): + """ + Test that we can create a new PurchaseOrder via the API + """ + + self.assignRole('purchase_order.add') + + self.post( + reverse('api-po-list'), + { + 'reference': '12345678', + 'supplier': 1, + 'description': 'A test purchase order', + }, + expected_code=201 + ) + class PurchaseOrderReceiveTest(OrderTest): """ @@ -607,3 +624,20 @@ class SalesOrderTest(OrderTest): # And the resource should no longer be available response = self.get(url, expected_code=404) + + def test_so_create(self): + """ + Test that we can create a new SalesOrder via the API + """ + + self.assignRole('sales_order.add') + + self.post( + reverse('api-so-list'), + { + 'reference': '1234566778', + 'customer': 4, + 'description': 'A test sales order', + }, + expected_code=201 + ) From d47b32e4aa271f382a8fb6a9228e880d379c5e4e Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 16 Oct 2021 14:08:53 +1100 Subject: [PATCH 09/26] Adds function to complete multiple build outputs via the API --- InvenTree/templates/js/dynamic/inventree.js | 5 + InvenTree/templates/js/translated/build.js | 155 +++++++++++++++++++- 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/dynamic/inventree.js b/InvenTree/templates/js/dynamic/inventree.js index 0324d72e3c..d06399c61e 100644 --- a/InvenTree/templates/js/dynamic/inventree.js +++ b/InvenTree/templates/js/dynamic/inventree.js @@ -290,3 +290,8 @@ function loadBrandIcon(element, name) { element.addClass('fab fa-' + name); } } + +// Convenience function to determine if an element exists +$.fn.exists = function() { + return this.length !== 0; +} diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 96594ed8dd..2515cbc346 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -324,6 +324,143 @@ function unallocateStock(build_id, options={}) { } } }); +} + + +/** + * Launch a modal form to complete selected build outputs + */ +function completeBuildOutputs(build_id, outputs, options={}) { + + if (outputs.length == 0) { + showAlertDialog( + '{% trans "Select Build Outputs" %}', + '{% trans "At least one build output must be selected" %}', + ); + return; + } + + // Render a single build output (StockItem) + function renderBuildOutput(output, opts={}) { + var pk = output.pk; + + var quantity = ''; + + if (output.quantity == 1 && output.serial) { + quantity = `{% trans "Serial Number" %}: ${output.serial}`; + } else { + quantity = `{% trans "Quantity" %}: ${output.quantity}`; + } + + var buttons = `
`; + + buttons += makeIconButton('fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}'); + + buttons += '
'; + + var html = ` + + ${quantity} + ${buttons} + `; + + return html; + } + + // Construct table entries + var table_entries = ''; + + outputs.forEach(function(output) { + table_entries += renderBuildOutput(output); + }); + + var html = ` + + + + + + + ${table_entries} + +
{% trans "Output" %}
`; + + constructForm(`/api/build/${build_id}/complete/`, { + method: 'POST', + preFormContent: html, + fields: { + status: {}, + location: {}, + }, + confirm: true, + title: '{% trans "Complete Build Outputs" %}', + afterRender: function(fields, opts) { + // Setup callbacks to remove outputs + $(opts.modal).find('.button-row-remove').click(function() { + var pk = $(this).attr('pk'); + + $(opts.modal).find(`#output_row_${pk}`).remove(); + }); + }, + onSubmit: function(fields, opts) { + + // Extract data elements from the form + var data = { + outputs: [], + status: getFormFieldValue('status', {}, opts), + location: getFormFieldValue('location', {}, opts), + }; + + var output_pk_values = []; + + outputs.forEach(function(output) { + var pk = output.pk; + + var row = $(opts.modal).find(`#output_row_${pk}`); + + if (row.exists()) { + data.outputs.push({ + output: pk, + }); + output_pk_values.push(pk); + } else { + console.log(`Could not find row for ${pk}`); + } + }); + + // Provide list of nested values + opts.nested = { + 'outputs': output_pk_values, + }; + + inventreePut( + opts.url, + data, + { + method: 'POST', + success: function(response) { + // Hide the modal + $(opts.modal).modal('hide'); + + if (options.success) { + options.success(response); + } + }, + error: function(xhr) { + switch (xhr.status) { + case 400: + handleFormErrors(xhr.responseJSON, fields, opts); + break; + default: + $(opts.modal).modal('hide'); + showApiError(xhr); + break; + } + } + } + ) + } + }); } @@ -453,6 +590,7 @@ function loadBuildOutputTable(build_info, options={}) { // TODO var todo = "Work out which stock items we need to allocate and launch the form"; + /* allocateStockToBuild( build_info.pk, @@ -475,7 +613,19 @@ function loadBuildOutputTable(build_info, options={}) { $(table).find('.button-output-complete').click(function() { var pk = $(this).attr('pk'); - var todo = "write function to complete build output(s)"; + var output = $(table).bootstrapTable('getRowByUniqueId', pk); + + completeBuildOutputs( + build_info.pk, + [ + output, + ], + { + success: function() { + $(table).bootstrapTable('refresh'); + } + } + ); }); // Callback for the "delete" button @@ -554,6 +704,8 @@ function loadBuildOutputTable(build_info, options={}) { field: 'allocated', title: '{% trans "Allocated" %}', formatter: function(value, row) { + + // Render a "progress" row return "todo"; } }, @@ -579,6 +731,7 @@ function loadBuildOutputTable(build_info, options={}) { queryParams: filters, original: params, showColumns: true, + uniqueId: 'pk', name: 'build-outputs', sortable: true, search: true, From 14ef56785e6bab415e808da83273ac4638906199 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 16 Oct 2021 14:27:02 +1100 Subject: [PATCH 10/26] Add option to select multiple build outputs - Can "complete" these outputs --- InvenTree/build/templates/build/detail.html | 54 +++++++++++++++++---- InvenTree/build/templates/build/navbar.html | 15 ++++-- InvenTree/templates/js/translated/build.js | 8 ++- InvenTree/templates/js/translated/tables.js | 6 +-- 4 files changed, 65 insertions(+), 18 deletions(-) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index b7cdf12921..eca8ffc1a0 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -213,19 +213,31 @@
- {% if not build.is_complete %}

{% trans "Incomplete Build Outputs" %}

- {% if build.active %} -
- - +
+
+ {% if build.active %} +
+ + + + {% endif %} +
+
- +
{% for item in build.incomplete_outputs %} {% include "build/allocation_card.html" with item=item tracked_items=build.has_tracked_bom_items %} @@ -233,10 +245,10 @@
- {% endif %}
- {% endif %} +
+

{% trans "Completed Build Outputs" %} @@ -336,6 +348,28 @@ inventreeGet( {% if build.active %} loadBuildOutputTable(build_info); + linkButtonsToSelection( + '#build-output-table', + [ + '#output-options', + '#multi-output-complete', + ] + ); + + $('#multi-output-complete').click(function() { + var outputs = $('#build-output-table').bootstrapTable('getSelections'); + + completeBuildOutputs( + build_info.pk, + outputs, + { + success: function() { + $('#build-output-table').bootstrapTable('refresh'); + } + } + ); + }); + {% endif %} {% for item in build.incomplete_outputs %} diff --git a/InvenTree/build/templates/build/navbar.html b/InvenTree/build/templates/build/navbar.html index e4c4fe4e50..de68558d8f 100644 --- a/InvenTree/build/templates/build/navbar.html +++ b/InvenTree/build/templates/build/navbar.html @@ -25,10 +25,19 @@ {% endif %} -
  • + {% if not build.is_complete %} +
  • - - {% trans "Build Outputs" %} + + {% trans "In Progress Items" %} + +
  • + {% endif %} + +
  • + + + {% trans "Completed Items" %}
  • diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 2515cbc346..7fb10bd743 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -423,8 +423,6 @@ function completeBuildOutputs(build_id, outputs, options={}) { output: pk, }); output_pk_values.push(pk); - } else { - console.log(`Could not find row for ${pk}`); } }); @@ -753,6 +751,12 @@ function loadBuildOutputTable(build_info, options={}) { setupBuildOutputButtonCallbacks(); }, columns: [ + { + title: '', + visible: true, + checkbox: true, + switchable: false, + }, { field: 'part', title: '{% trans "Part" %}', diff --git a/InvenTree/templates/js/translated/tables.js b/InvenTree/templates/js/translated/tables.js index 3138c1e73d..fed7a5d980 100644 --- a/InvenTree/templates/js/translated/tables.js +++ b/InvenTree/templates/js/translated/tables.js @@ -56,10 +56,10 @@ function enableButtons(elements, enabled) { } +/* Link a bootstrap-table object to one or more buttons. + * The buttons will only be enabled if there is at least one row selected + */ function linkButtonsToSelection(table, buttons) { - /* Link a bootstrap-table object to one or more buttons. - * The buttons will only be enabled if there is at least one row selected - */ if (typeof table === 'string') { table = $(table); From 283bf1682f5e5df55ffab24b10ba415221c86400 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 11:25:33 +1100 Subject: [PATCH 11/26] Remove old views / forms --- InvenTree/build/forms.py | 53 ------ .../templates/build/allocation_card.html | 51 ------ .../templates/build/complete_output.html | 53 ------ InvenTree/build/urls.py | 1 - InvenTree/build/views.py | 170 ------------------ 5 files changed, 328 deletions(-) delete mode 100644 InvenTree/build/templates/build/allocation_card.html delete mode 100644 InvenTree/build/templates/build/complete_output.html diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index bc7bdd50f5..f385db7f9c 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -155,59 +155,6 @@ class CompleteBuildForm(HelperForm): ] -class CompleteBuildOutputForm(HelperForm): - """ - Form for completing a single build output - """ - - field_prefix = { - 'serial_numbers': 'fa-hashtag', - } - - field_placeholder = { - } - - location = forms.ModelChoiceField( - queryset=StockLocation.objects.all(), - label=_('Location'), - help_text=_('Location of completed parts'), - ) - - stock_status = forms.ChoiceField( - label=_('Status'), - help_text=_('Build output stock status'), - initial=StockStatus.OK, - choices=StockStatus.items(), - ) - - confirm_incomplete = forms.BooleanField( - required=False, - label=_('Confirm incomplete'), - help_text=_("Confirm completion with incomplete stock allocation") - ) - - confirm = forms.BooleanField(required=True, label=_('Confirm'), help_text=_('Confirm build completion')) - - output = forms.ModelChoiceField( - queryset=StockItem.objects.all(), # Queryset is narrowed in the view - widget=forms.HiddenInput(), - ) - - class Meta: - model = Build - fields = [ - 'location', - 'output', - 'stock_status', - 'confirm', - 'confirm_incomplete', - ] - - def __init__(self, *args, **kwargs): - - super().__init__(*args, **kwargs) - - class CancelBuildForm(HelperForm): """ Form for cancelling a build """ diff --git a/InvenTree/build/templates/build/allocation_card.html b/InvenTree/build/templates/build/allocation_card.html deleted file mode 100644 index 3ce4a52aeb..0000000000 --- a/InvenTree/build/templates/build/allocation_card.html +++ /dev/null @@ -1,51 +0,0 @@ -{% load i18n %} -{% load inventree_extras %} - -{% define item.pk as pk %} - - \ No newline at end of file diff --git a/InvenTree/build/templates/build/complete_output.html b/InvenTree/build/templates/build/complete_output.html deleted file mode 100644 index d03885774f..0000000000 --- a/InvenTree/build/templates/build/complete_output.html +++ /dev/null @@ -1,53 +0,0 @@ -{% extends "modal_form.html" %} -{% load inventree_extras %} -{% load i18n %} - -{% block pre_form_content %} - -{% if not build.has_tracked_bom_items %} -{% elif fully_allocated %} -
    - {% trans "Stock allocation is complete for this output" %} -
    -{% else %} -
    -

    {% trans "Stock allocation is incomplete" %}

    - -
    -
    - -
    -
    -
      - {% for part in unallocated_parts %} -
    • - {% include "hover_image.html" with image=part.image %} {{ part }} -
    • - {% endfor %} -
    -
    -
    -
    -
    -
    -{% endif %} - -
    -
    - {% trans "The following items will be created" %} -
    -
    - {% include "hover_image.html" with image=build.part.image %} - {% if output.serialized %} - {{ output.part.full_name }} - {% trans "Serial Number" %} {{ output.serial }} - {% else %} - {% decimal output.quantity %} x {{ output.part.full_name }} - {% endif %} -
    -
    - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index d80b16056c..8ea339ae26 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -11,7 +11,6 @@ build_detail_urls = [ url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'), 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'^complete/', views.BuildComplete.as_view(), name='build-complete'), url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 37ec567f1a..5499b48395 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -278,176 +278,6 @@ class BuildComplete(AjaxUpdateView): } -class BuildOutputComplete(AjaxUpdateView): - """ - View to mark a particular build output as Complete. - - - Notifies the user of which parts will be removed from stock. - - Assignes (tracked) allocated items from stock to the build output - - Deletes pending BuildItem objects - """ - - model = Build - form_class = forms.CompleteBuildOutputForm - context_object_name = "build" - ajax_form_title = _("Complete Build Output") - ajax_template_name = "build/complete_output.html" - - def get_form(self): - - build = self.get_object() - - form = super().get_form() - - # Extract the build output object - output = None - output_id = form['output'].value() - - try: - output = StockItem.objects.get(pk=output_id) - except (ValueError, StockItem.DoesNotExist): - pass - - if output: - if build.isFullyAllocated(output): - form.fields['confirm_incomplete'].widget = HiddenInput() - - return form - - def validate(self, build, form, **kwargs): - """ - Custom validation steps for the BuildOutputComplete" form - """ - - data = form.cleaned_data - - output = data.get('output', None) - - stock_status = data.get('stock_status', StockStatus.OK) - - # Any "invalid" stock status defaults to OK - try: - stock_status = int(stock_status) - except (ValueError): - stock_status = StockStatus.OK - - if int(stock_status) not in StockStatus.keys(): - form.add_error('stock_status', _('Invalid stock status value selected')) - - if output: - - quantity = data.get('quantity', None) - - if quantity and quantity > output.quantity: - form.add_error('quantity', _('Quantity to complete cannot exceed build output quantity')) - - if not build.isFullyAllocated(output): - confirm = str2bool(data.get('confirm_incomplete', False)) - - if not confirm: - form.add_error('confirm_incomplete', _('Confirm completion of incomplete build')) - - else: - form.add_error(None, _('Build output must be specified')) - - def get_initial(self): - """ Get initial form data for the CompleteBuild form - - - If the part being built has a default location, pre-select that location - """ - - initials = super().get_initial() - build = self.get_object() - - if build.part.default_location is not None: - try: - location = StockLocation.objects.get(pk=build.part.default_location.id) - initials['location'] = location - except StockLocation.DoesNotExist: - pass - - output = self.get_param('output', None) - - if output: - try: - output = StockItem.objects.get(pk=output) - except (ValueError, StockItem.DoesNotExist): - output = None - - # Output has not been supplied? Try to "guess" - if not output: - - incomplete = build.get_build_outputs(complete=False) - - if incomplete.count() == 1: - output = incomplete[0] - - if output is not None: - initials['output'] = output - - initials['location'] = build.destination - - return initials - - def get_context_data(self, **kwargs): - """ - Get context data for passing to the rendered form - - - Build information is required - """ - - build = self.get_object() - - context = {} - - # Build object - context['build'] = build - - form = self.get_form() - - output = form['output'].value() - - if output: - try: - output = StockItem.objects.get(pk=output) - context['output'] = output - context['fully_allocated'] = build.isFullyAllocated(output) - context['allocated_parts'] = build.allocatedParts(output) - context['unallocated_parts'] = build.unallocatedParts(output) - except (ValueError, StockItem.DoesNotExist): - pass - - return context - - def save(self, build, form, **kwargs): - - data = form.cleaned_data - - location = data.get('location', None) - output = data.get('output', None) - stock_status = data.get('stock_status', StockStatus.OK) - - # Any "invalid" stock status defaults to OK - try: - stock_status = int(stock_status) - except (ValueError): - stock_status = StockStatus.OK - - # Complete the build output - build.complete_build_output( - output, - self.request.user, - location=location, - status=stock_status, - ) - - def get_data(self): - """ Provide feedback data back to the form """ - return { - 'success': _('Build output completed') - } - - class BuildDetail(InvenTreeRoleMixin, DetailView): """ Detail view of a single Build object. """ From 9d2273c1cd442ba04974ef87bc6d82b528bf2e9f Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 11:25:53 +1100 Subject: [PATCH 12/26] Further work on build output scripts --- .../build/templates/build/build_base.html | 4 +- InvenTree/build/templates/build/detail.html | 32 +- InvenTree/build/templates/build/navbar.html | 12 +- InvenTree/templates/base.html | 1 - InvenTree/templates/js/translated/build.js | 330 +++--------------- 5 files changed, 60 insertions(+), 319 deletions(-) diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index d821b1743e..1731e00403 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -153,8 +153,8 @@ src="{% static 'img/blank_image.png' %}" {% endif %} - - {% trans "Progress" %} + + {% trans "Completed" %} {{ build.completed }} / {{ build.quantity }} {% if build.parent %} diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index eca8ffc1a0..704479673b 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -63,10 +63,17 @@ {% build_status_label build.status %} - - {% trans "Progress" %} + + {% trans "Completed" %} {{ build.completed }} / {{ build.quantity }} + {% if True or build.active and build.has_untracked_bom_items %} + + + {% trans "Allocated Parts" %} + + + {% endif %} {% if build.batch %} @@ -237,13 +244,6 @@

    - -
    - {% for item in build.incomplete_outputs %} - {% include "build/allocation_card.html" with item=item tracked_items=build.has_tracked_bom_items %} - {% endfor %} -
    -
    @@ -371,23 +371,11 @@ inventreeGet( }); {% endif %} - - {% for item in build.incomplete_outputs %} - // Get the build output as a javascript object - inventreeGet('{% url 'api-stock-detail' item.pk %}', {}, - { - success: function(response) { - loadBuildOutputAllocationTable(build_info, response); - } - } - ); - {% endfor %} - {% if build.has_untracked_bom_items %} + {% if build.active and build.has_untracked_bom_items %} // Load allocation table for un-tracked parts loadBuildOutputAllocationTable(build_info, null); {% endif %} - } } ); diff --git a/InvenTree/build/templates/build/navbar.html b/InvenTree/build/templates/build/navbar.html index de68558d8f..9b159503dc 100644 --- a/InvenTree/build/templates/build/navbar.html +++ b/InvenTree/build/templates/build/navbar.html @@ -19,25 +19,25 @@ {% if build.active %}
  • - + {% trans "Allocate Stock" %}
  • {% endif %} {% if not build.is_complete %} -
  • +
  • - - {% trans "In Progress Items" %} + + {% trans "Pending Outputs" %}
  • {% endif %} -
  • +
  • - {% trans "Completed Items" %} + {% trans "Completed Outputs" %}
  • diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index c2316ce4b0..98d83aa2d9 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -143,7 +143,6 @@ - diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 7fb10bd743..9cee9bb403 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -24,7 +24,6 @@ loadAllocationTable, loadBuildOrderAllocationTable, loadBuildOutputAllocationTable, - loadBuildPartsTable, loadBuildTable, */ @@ -243,21 +242,6 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) { ); }); - $(panel).find(`#button-output-complete-${outputId}`).click(function() { - - var pk = $(this).attr('pk'); - - launchModalForm( - `/build/${buildId}/complete-output/`, - { - data: { - output: pk, - }, - reload: true, - } - ); - }); - $(panel).find(`#button-output-unallocate-${outputId}`).click(function() { var pk = $(this).attr('pk'); @@ -459,14 +443,13 @@ function completeBuildOutputs(build_id, outputs, options={}) { ) } }); - } +/** + * Load a table showing all the BuildOrder allocations for a given part + */ function loadBuildOrderAllocationTable(table, options={}) { - /** - * Load a table showing all the BuildOrder allocations for a given part - */ options.params['part_detail'] = true; options.params['build_detail'] = true; @@ -566,9 +549,12 @@ function loadBuildOutputTable(build_info, options={}) { // Construct a list of "tracked" BOM items var tracked_bom_items = []; + var has_tracked_items = false; + build_info.bom_items.forEach(function(bom_item) { if (bom_item.sub_part_detail.trackable) { tracked_bom_items.push(bom_item); + has_tracked_items = true; }; }); @@ -659,83 +645,26 @@ function loadBuildOutputTable(build_info, options={}) { element.html(html); - var todo = "refactor the following fields, they are shared with the 'untracked' allocation table!"; - - $(`#${sub_table_id}`).bootstrapTable({ - data: tracked_bom_items, - showHeader: true, - columns: [ - { - field: 'part', - title: '{% trans "Required Part" %}', - formatter: function(value, row) { - var part = row.sub_part_detail; - - var url = `/part/${part.pk}/`; - var thumb = part.thumbnail || row.image; - var name = part.full_name; - - var html = imageHoverIcon(thumb) + renderLink(name, url) + makePartIcons(part); - - if (row.substitutes && row.substitutes.length > 0) { - html += makeIconBadge('fa-exchange-alt', '{% trans "Substitute parts available" %}'); - } - - if (row.allow_variants) { - html += makeIconBadge('fa-sitemap', '{% trans "Variant stock allowed" %}'); - } - - return html; - } - }, - { - field: 'reference', - title: '{% trans "Reference" %}', - sortable: true, - }, - { - field: 'quantity', - title: '{% trans "Quantity Per Item" %}', - sortable: true, - }, - { - field: 'allocated', - title: '{% trans "Allocated" %}', - formatter: function(value, row) { - - // Render a "progress" row - return "todo"; - } - }, - { - field: 'actions', - title: '', - formatter: function(value, row) { - var html = `
    `; - - html += "todo"; - - html += `
    `; - - return html; - } - } - ] - }); + loadBuildOutputAllocationTable( + build_info, + row, + { + table: `#${sub_table_id}`, + } + ); } $(table).inventreeTable({ url: '{% url "api-stock-list" %}', queryParams: filters, original: params, - showColumns: true, + showColumns: false, uniqueId: 'pk', name: 'build-outputs', sortable: true, - search: true, + search: false, sidePagination: 'server', - detailViewAlign: 'right', - detailView: build_info.tracked_parts || false, + detailView: has_tracked_items, detailViewByClick: true, detailFilter: function(index, row) { return true; @@ -749,6 +678,9 @@ function loadBuildOutputTable(build_info, options={}) { onPostBody: function() { // Add callbacks for the buttons setupBuildOutputButtonCallbacks(); + + $(table).bootstrapTable('expandAllRows'); + // $(table).bootstrapTable('collapseAllRows'); }, columns: [ { @@ -786,14 +718,16 @@ function loadBuildOutputTable(build_info, options={}) { }, { field: 'allocated', - title: '{% trans "Allocated" %}', + title: '{% trans "Allocated Parts" %}', + visible: has_tracked_items, formatter: function(value, row) { - return "TODO"; + return `
    `; } }, { field: 'actions', title: '', + switchable: false, formatter: function(value, row) { return makeBuildOutputButtons( row.pk, @@ -986,6 +920,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { }, name: 'build-allocation', uniqueId: 'sub_part', + search: options.search || false, onPostBody: setupCallbacks, onLoadSuccess: function(tableData) { // Once the BOM data are loaded, request allocation data for this build output @@ -1062,23 +997,27 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { $(table).bootstrapTable('updateByUniqueId', key, tableRow, true); } - // Update the total progress for this build output - var buildProgress = $(`#allocation-panel-${outputId}`).find($(`#output-progress-${outputId}`)); + // Update the progress bar for this build output + var build_progress = $(`#output-progress-${outputId}`); - if (totalLines > 0) { + if (build_progress.exists()) { + if (totalLines > 0) { - var progress = makeProgressBar( - allocatedLines, - totalLines - ); - - buildProgress.html(progress); + var progress = makeProgressBar( + allocatedLines, + totalLines + ); + + build_progress.html(progress); + } else { + build_progress.html(''); + } + } else { - buildProgress.html(''); + console.log(`WARNING: Could not find progress bar for output ${outputId}`); } // Update the available actions for this build output - makeBuildOutputActionButtons(output, buildInfo, totalLines); } } @@ -1650,9 +1589,10 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { } - +/* + * Display a table of Build orders + */ function loadBuildTable(table, options) { - // Display a table of Build objects var params = options.params || {}; @@ -1919,190 +1859,4 @@ function loadAllocationTable(table, part_id, part, url, required, button) { } }); }); - -} - - -function loadBuildPartsTable(table, options={}) { - /** - * Display a "required parts" table for build view. - * - * This is a simplified BOM view: - * - Does not display sub-bom items - * - Does not allow editing of BOM items - * - * Options: - * - * part: Part ID - * build: Build ID - * build_quantity: Total build quantity - * build_remaining: Number of items remaining - */ - - // Query params - var params = { - sub_part_detail: true, - part: options.part, - }; - - var filters = {}; - - if (!options.disableFilters) { - filters = loadTableFilters('bom'); - } - - setupFilterList('bom', $(table)); - - for (var key in params) { - filters[key] = params[key]; - } - - function setupTableCallbacks() { - // Register button callbacks once the table data are loaded - - // Callback for 'buy' button - $(table).find('.button-buy').click(function() { - var pk = $(this).attr('pk'); - - launchModalForm('{% url "order-parts" %}', { - data: { - parts: [ - pk, - ] - } - }); - }); - - // Callback for 'build' button - $(table).find('.button-build').click(function() { - var pk = $(this).attr('pk'); - - newBuildOrder({ - part: pk, - parent: options.build, - }); - }); - } - - var columns = [ - { - field: 'sub_part', - title: '{% trans "Part" %}', - switchable: false, - sortable: true, - formatter: function(value, row) { - var url = `/part/${row.sub_part}/`; - var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, url); - - var sub_part = row.sub_part_detail; - - html += makePartIcons(row.sub_part_detail); - - // Display an extra icon if this part is an assembly - if (sub_part.assembly) { - var text = ``; - - html += renderLink(text, `/part/${row.sub_part}/bom/`); - } - - return html; - } - }, - { - field: 'sub_part_detail.description', - title: '{% trans "Description" %}', - }, - { - field: 'reference', - title: '{% trans "Reference" %}', - searchable: true, - sortable: true, - }, - { - field: 'quantity', - title: '{% trans "Quantity" %}', - sortable: true - }, - { - sortable: true, - switchable: false, - field: 'sub_part_detail.stock', - title: '{% trans "Available" %}', - formatter: function(value, row) { - return makeProgressBar( - value, - row.quantity * options.build_remaining, - { - id: `part-progress-${row.part}` - } - ); - }, - sorter: function(valA, valB, rowA, rowB) { - if (rowA.received == 0 && rowB.received == 0) { - return (rowA.quantity > rowB.quantity) ? 1 : -1; - } - - var progressA = parseFloat(rowA.sub_part_detail.stock) / (rowA.quantity * options.build_remaining); - var progressB = parseFloat(rowB.sub_part_detail.stock) / (rowB.quantity * options.build_remaining); - - return (progressA < progressB) ? 1 : -1; - } - }, - { - field: 'actions', - title: '{% trans "Actions" %}', - switchable: false, - formatter: function(value, row) { - - // Generate action buttons against the part - var html = `
    `; - - if (row.sub_part_detail.assembly) { - html += makeIconButton('fa-tools icon-blue', 'button-build', row.sub_part, '{% trans "Build stock" %}'); - } - - if (row.sub_part_detail.purchaseable) { - html += makeIconButton('fa-shopping-cart icon-blue', 'button-buy', row.sub_part, '{% trans "Order stock" %}'); - } - - html += `
    `; - - return html; - } - } - ]; - - table.inventreeTable({ - url: '{% url "api-bom-list" %}', - showColumns: true, - name: 'build-parts', - sortable: true, - search: true, - onPostBody: setupTableCallbacks, - rowStyle: function(row) { - var classes = []; - - // Shade rows differently if they are for different parent parts - if (row.part != options.part) { - classes.push('rowinherited'); - } - - if (row.validated) { - classes.push('rowvalid'); - } else { - classes.push('rowinvalid'); - } - - return { - classes: classes.join(' '), - }; - }, - formatNoMatches: function() { - return '{% trans "No BOM items found" %}'; - }, - clickToSelect: true, - queryParams: filters, - original: params, - columns: columns, - }); } From 6c1dad8d91b07889128b76bafa4f9bd9d892a7de Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 11:26:08 +1100 Subject: [PATCH 13/26] Consolidate javascript for sidenavs --- .../static/script/inventree/sidenav.js | 249 ----------------- InvenTree/templates/js/dynamic/nav.js | 250 ++++++++++++++++++ 2 files changed, 250 insertions(+), 249 deletions(-) delete mode 100644 InvenTree/InvenTree/static/script/inventree/sidenav.js diff --git a/InvenTree/InvenTree/static/script/inventree/sidenav.js b/InvenTree/InvenTree/static/script/inventree/sidenav.js deleted file mode 100644 index eca19076f2..0000000000 --- a/InvenTree/InvenTree/static/script/inventree/sidenav.js +++ /dev/null @@ -1,249 +0,0 @@ -function loadTree(url, tree, options={}) { - /* Load the side-nav tree view - - Args: - url: URL to request tree data - tree: html ref to treeview - options: - data: data object to pass to the AJAX request - selected: ID of currently selected item - name: name of the tree - */ - - var data = {}; - - if (options.data) { - data = options.data; - } - - var key = "inventree-sidenav-items-"; - - if (options.name) { - key += options.name; - } - - $.ajax({ - url: url, - type: 'get', - dataType: 'json', - data: data, - success: function (response) { - if (response.tree) { - $(tree).treeview({ - data: response.tree, - enableLinks: true, - showTags: true, - }); - - if (localStorage.getItem(key)) { - var saved_exp = localStorage.getItem(key).split(","); - - // Automatically expand the desired notes - for (var q = 0; q < saved_exp.length; q++) { - $(tree).treeview('expandNode', parseInt(saved_exp[q])); - } - } - - // Setup a callback whenever a node is toggled - $(tree).on('nodeExpanded nodeCollapsed', function(event, data) { - - // Record the entire list of expanded items - var expanded = $(tree).treeview('getExpanded'); - - var exp = []; - - for (var i = 0; i < expanded.length; i++) { - exp.push(expanded[i].nodeId); - } - - // Save the expanded nodes - localStorage.setItem(key, exp); - }); - } - }, - error: function (xhr, ajaxOptions, thrownError) { - //TODO - } - }); -} - - -/** - * Initialize navigation tree display - */ -function initNavTree(options) { - - var resize = true; - - if ('resize' in options) { - resize = options.resize; - } - - var label = options.label || 'nav'; - - var stateLabel = `${label}-tree-state`; - var widthLabel = `${label}-tree-width`; - - var treeId = options.treeId || '#sidenav-left'; - var toggleId = options.toggleId; - - // Initially hide the tree - $(treeId).animate({ - width: '0px', - }, 0, function() { - - if (resize) { - $(treeId).resizable({ - minWidth: '0px', - maxWidth: '500px', - handles: 'e, se', - grid: [5, 5], - stop: function(event, ui) { - var width = Math.round(ui.element.width()); - - if (width < 75) { - $(treeId).animate({ - width: '0px' - }, 50); - - localStorage.setItem(stateLabel, 'closed'); - } else { - localStorage.setItem(stateLabel, 'open'); - localStorage.setItem(widthLabel, `${width}px`); - } - } - }); - } - - var state = localStorage.getItem(stateLabel); - var width = localStorage.getItem(widthLabel) || '300px'; - - if (state && state == 'open') { - - $(treeId).animate({ - width: width, - }, 50); - } - }); - - // Register callback for 'toggle' button - if (toggleId) { - - $(toggleId).click(function() { - - var state = localStorage.getItem(stateLabel) || 'closed'; - var width = localStorage.getItem(widthLabel) || '300px'; - - if (state == 'open') { - $(treeId).animate({ - width: '0px' - }, 50); - - localStorage.setItem(stateLabel, 'closed'); - } else { - $(treeId).animate({ - width: width, - }, 50); - - localStorage.setItem(stateLabel, 'open'); - } - }); - } -} - - -/** - * Handle left-hand icon menubar display - */ -function enableNavbar(options) { - - var resize = true; - - if ('resize' in options) { - resize = options.resize; - } - - var label = options.label || 'nav'; - - label = `navbar-${label}`; - - var stateLabel = `${label}-state`; - var widthLabel = `${label}-width`; - - var navId = options.navId || '#sidenav-right'; - - var toggleId = options.toggleId; - - // Extract the saved width for this element - $(navId).animate({ - width: '45px', - 'min-width': '45px', - display: 'block', - }, 50, function() { - - // Make the navbar resizable - if (resize) { - $(navId).resizable({ - minWidth: options.minWidth || '100px', - maxWidth: options.maxWidth || '500px', - handles: 'e, se', - grid: [5, 5], - stop: function(event, ui) { - // Record the new width - var width = Math.round(ui.element.width()); - - // Reasonably narrow? Just close it! - if (width <= 75) { - $(navId).animate({ - width: '45px' - }, 50); - - localStorage.setItem(stateLabel, 'closed'); - } else { - localStorage.setItem(widthLabel, `${width}px`); - localStorage.setItem(stateLabel, 'open'); - } - } - }); - } - - var state = localStorage.getItem(stateLabel); - - var width = localStorage.getItem(widthLabel) || '250px'; - - if (state && state == 'open') { - - $(navId).animate({ - width: width - }, 100); - } - - }); - - // Register callback for 'toggle' button - if (toggleId) { - - $(toggleId).click(function() { - - var state = localStorage.getItem(stateLabel) || 'closed'; - var width = localStorage.getItem(widthLabel) || '250px'; - - if (state == 'open') { - $(navId).animate({ - width: '45px', - minWidth: '45px', - }, 50); - - localStorage.setItem(stateLabel, 'closed'); - - } else { - - $(navId).animate({ - 'width': width - }, 50); - - localStorage.setItem(stateLabel, 'open'); - } - }); - } -} \ No newline at end of file diff --git a/InvenTree/templates/js/dynamic/nav.js b/InvenTree/templates/js/dynamic/nav.js index cf652724ed..fb26c841c8 100644 --- a/InvenTree/templates/js/dynamic/nav.js +++ b/InvenTree/templates/js/dynamic/nav.js @@ -113,3 +113,253 @@ function onPanelLoad(panel, callback) { }); } + +function loadTree(url, tree, options={}) { + /* Load the side-nav tree view + + Args: + url: URL to request tree data + tree: html ref to treeview + options: + data: data object to pass to the AJAX request + selected: ID of currently selected item + name: name of the tree + */ + + var data = {}; + + if (options.data) { + data = options.data; + } + + var key = "inventree-sidenav-items-"; + + if (options.name) { + key += options.name; + } + + $.ajax({ + url: url, + type: 'get', + dataType: 'json', + data: data, + success: function (response) { + if (response.tree) { + $(tree).treeview({ + data: response.tree, + enableLinks: true, + showTags: true, + }); + + if (localStorage.getItem(key)) { + var saved_exp = localStorage.getItem(key).split(","); + + // Automatically expand the desired notes + for (var q = 0; q < saved_exp.length; q++) { + $(tree).treeview('expandNode', parseInt(saved_exp[q])); + } + } + + // Setup a callback whenever a node is toggled + $(tree).on('nodeExpanded nodeCollapsed', function(event, data) { + + // Record the entire list of expanded items + var expanded = $(tree).treeview('getExpanded'); + + var exp = []; + + for (var i = 0; i < expanded.length; i++) { + exp.push(expanded[i].nodeId); + } + + // Save the expanded nodes + localStorage.setItem(key, exp); + }); + } + }, + error: function (xhr, ajaxOptions, thrownError) { + //TODO + } + }); +} + + +/** + * Initialize navigation tree display + */ +function initNavTree(options) { + + var resize = true; + + if ('resize' in options) { + resize = options.resize; + } + + var label = options.label || 'nav'; + + var stateLabel = `${label}-tree-state`; + var widthLabel = `${label}-tree-width`; + + var treeId = options.treeId || '#sidenav-left'; + var toggleId = options.toggleId; + + // Initially hide the tree + $(treeId).animate({ + width: '0px', + }, 0, function() { + + if (resize) { + $(treeId).resizable({ + minWidth: '0px', + maxWidth: '500px', + handles: 'e, se', + grid: [5, 5], + stop: function(event, ui) { + var width = Math.round(ui.element.width()); + + if (width < 75) { + $(treeId).animate({ + width: '0px' + }, 50); + + localStorage.setItem(stateLabel, 'closed'); + } else { + localStorage.setItem(stateLabel, 'open'); + localStorage.setItem(widthLabel, `${width}px`); + } + } + }); + } + + var state = localStorage.getItem(stateLabel); + var width = localStorage.getItem(widthLabel) || '300px'; + + if (state && state == 'open') { + + $(treeId).animate({ + width: width, + }, 50); + } + }); + + // Register callback for 'toggle' button + if (toggleId) { + + $(toggleId).click(function() { + + var state = localStorage.getItem(stateLabel) || 'closed'; + var width = localStorage.getItem(widthLabel) || '300px'; + + if (state == 'open') { + $(treeId).animate({ + width: '0px' + }, 50); + + localStorage.setItem(stateLabel, 'closed'); + } else { + $(treeId).animate({ + width: width, + }, 50); + + localStorage.setItem(stateLabel, 'open'); + } + }); + } +} + + +/** + * Handle left-hand icon menubar display + */ +function enableNavbar(options) { + + var resize = true; + + if ('resize' in options) { + resize = options.resize; + } + + var label = options.label || 'nav'; + + label = `navbar-${label}`; + + var stateLabel = `${label}-state`; + var widthLabel = `${label}-width`; + + var navId = options.navId || '#sidenav-right'; + + var toggleId = options.toggleId; + + // Extract the saved width for this element + $(navId).animate({ + width: '45px', + 'min-width': '45px', + display: 'block', + }, 50, function() { + + // Make the navbar resizable + if (resize) { + $(navId).resizable({ + minWidth: options.minWidth || '100px', + maxWidth: options.maxWidth || '500px', + handles: 'e, se', + grid: [5, 5], + stop: function(event, ui) { + // Record the new width + var width = Math.round(ui.element.width()); + + // Reasonably narrow? Just close it! + if (width <= 75) { + $(navId).animate({ + width: '45px' + }, 50); + + localStorage.setItem(stateLabel, 'closed'); + } else { + localStorage.setItem(widthLabel, `${width}px`); + localStorage.setItem(stateLabel, 'open'); + } + } + }); + } + + var state = localStorage.getItem(stateLabel); + + var width = localStorage.getItem(widthLabel) || '250px'; + + if (state && state == 'open') { + + $(navId).animate({ + width: width + }, 100); + } + + }); + + // Register callback for 'toggle' button + if (toggleId) { + + $(toggleId).click(function() { + + var state = localStorage.getItem(stateLabel) || 'closed'; + var width = localStorage.getItem(widthLabel) || '250px'; + + if (state == 'open') { + $(navId).animate({ + width: '45px', + minWidth: '45px', + }, 50); + + localStorage.setItem(stateLabel, 'closed'); + + } else { + + $(navId).animate({ + 'width': width + }, 50); + + localStorage.setItem(stateLabel, 'open'); + } + }); + } +} \ No newline at end of file From 90625af39d80d4e6b8e675abadab24aa93df9a48 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 11:34:44 +1100 Subject: [PATCH 14/26] Re-enable the "allocate" stock button --- InvenTree/build/templates/build/detail.html | 8 +++++- InvenTree/templates/js/translated/build.js | 28 +++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 704479673b..e2bf2358de 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -374,7 +374,13 @@ inventreeGet( {% if build.active and build.has_untracked_bom_items %} // Load allocation table for un-tracked parts - loadBuildOutputAllocationTable(build_info, null); + loadBuildOutputAllocationTable( + build_info, + null, + { + search: true, + } + ); {% endif %} } } diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 9cee9bb403..6ab7482dd8 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -574,13 +574,29 @@ function loadBuildOutputTable(build_info, options={}) { // TODO var todo = "Work out which stock items we need to allocate and launch the form"; + + // Find the "allocation" sub-table associated with this output + var subtable = $(`#output-sub-table-${pk}`); - /* - allocateStockToBuild( - build_info.pk, - build_info.part, + if (subtable.exists()) { + var rows = subtable.bootstrapTable('getSelections'); - )*/ + // None selected? Use all! + if (rows.length == 0) { + rows = subtable.bootstrapTable('getData'); + } + + allocateStockToBuild( + build_info.pk, + build_info.part, + rows, + { + output: pk, + } + ); + } else { + console.log(`WARNING: Could not locate sub-table for output ${pk}`); + } }); // Callack for the "unallocate" button @@ -665,7 +681,6 @@ function loadBuildOutputTable(build_info, options={}) { search: false, sidePagination: 'server', detailView: has_tracked_items, - detailViewByClick: true, detailFilter: function(index, row) { return true; }, @@ -1025,7 +1040,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { }, sortable: true, showColumns: false, - detailViewByClick: true, detailView: true, detailFilter: function(index, row) { return row.allocations != null; From 87dcaba0d87a68779cf415d6d6d8efaa4cb81692 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 20:23:39 +1100 Subject: [PATCH 15/26] Bump API version --- InvenTree/InvenTree/static/script/inventree/delay.js | 12 ------------ InvenTree/InvenTree/version.py | 5 ++++- 2 files changed, 4 insertions(+), 13 deletions(-) delete mode 100644 InvenTree/InvenTree/static/script/inventree/delay.js diff --git a/InvenTree/InvenTree/static/script/inventree/delay.js b/InvenTree/InvenTree/static/script/inventree/delay.js deleted file mode 100644 index 9070d0c5b9..0000000000 --- a/InvenTree/InvenTree/static/script/inventree/delay.js +++ /dev/null @@ -1,12 +0,0 @@ -var msDelay = 0; - -var delay = (function(){ - return function(callback, ms){ - clearTimeout(msDelay); - msDelay = setTimeout(callback, ms); - }; -})(); - -function cancelTimer(){ - clearTimeout(msDelay); -} \ No newline at end of file diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 5bbae8565e..48539713f2 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,11 +12,14 @@ import common.models INVENTREE_SW_VERSION = "0.6.0 dev" # InvenTree API version -INVENTREE_API_VERSION = 15 +INVENTREE_API_VERSION = 16 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v16 -> 2021-10-17 + - Adds API endpoint for completing build order outputs + v15 -> 2021-10-06 - Adds detail endpoint for SalesOrderAllocation model - Allows use of the API forms interface for adjusting SalesOrderAllocation objects From bd7fef720d715deb5af0226390d796ce3030a383 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 21:13:07 +1100 Subject: [PATCH 16/26] Enable "allocate" button only once the sub-table is expanded --- InvenTree/templates/js/translated/build.js | 144 +++---------------- InvenTree/templates/js/translated/helpers.js | 8 +- 2 files changed, 26 insertions(+), 126 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 6ab7482dd8..245904f5ca 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -24,6 +24,7 @@ loadAllocationTable, loadBuildOrderAllocationTable, loadBuildOutputAllocationTable, + loadBuildOutputTable, loadBuildTable, */ @@ -123,6 +124,9 @@ function makeBuildOutputButtons(output_id, build_info, options={}) { 'button-output-allocate', output_id, '{% trans "Allocate stock items to this build output" %}', + { + disabled: true, + } ); // Add a button to unallocate stock from this build output @@ -157,118 +161,6 @@ function makeBuildOutputButtons(output_id, build_info, options={}) { } -// TODO "delete me" - -function makeBuildOutputActionButtons(output, buildInfo, lines) { - /* Generate action buttons for a build output. - */ - - var todo = "delete this function ok"; - - var buildId = buildInfo.pk; - var partId = buildInfo.part; - - var outputId = 'untracked'; - - if (output) { - outputId = output.pk; - } - - var panel = `#allocation-panel-${outputId}`; - - function reloadTable() { - $(panel).find(`#allocation-table-${outputId}`).bootstrapTable('refresh'); - } - - // Find the div where the buttons will be displayed - var buildActions = $(panel).find(`#output-actions-${outputId}`); - - var html = `
    `; - - if (lines > 0) { - html += makeIconButton( - 'fa-sign-in-alt icon-blue', 'button-output-auto', outputId, - '{% trans "Allocate stock items to this build output" %}', - ); - } - - if (lines > 0) { - // Add a button to "cancel" the particular build output (unallocate) - html += makeIconButton( - 'fa-minus-circle icon-red', 'button-output-unallocate', outputId, - '{% trans "Unallocate stock from build output" %}', - ); - } - - if (output) { - - // Add a button to "complete" the particular build output - html += makeIconButton( - 'fa-check-circle icon-green', 'button-output-complete', outputId, - '{% trans "Complete build output" %}', - { - // disabled: true - } - ); - - // Add a button to "delete" the particular build output - html += makeIconButton( - 'fa-trash-alt icon-red', 'button-output-delete', outputId, - '{% trans "Delete build output" %}', - ); - - // TODO - Add a button to "destroy" the particular build output (mark as damaged, scrap) - } - - html += '
    '; - - buildActions.html(html); - - // Add callbacks for the buttons - $(panel).find(`#button-output-auto-${outputId}`).click(function() { - - var bom_items = $(panel).find(`#allocation-table-${outputId}`).bootstrapTable('getData'); - - // Launch modal dialog to perform auto-allocation - allocateStockToBuild( - buildId, - partId, - bom_items, - { - source_location: buildInfo.source_location, - output: outputId, - success: reloadTable, - } - ); - }); - - $(panel).find(`#button-output-unallocate-${outputId}`).click(function() { - - var pk = $(this).attr('pk'); - - unallocateStock(buildId, { - output: pk, - table: table, - }); - }); - - $(panel).find(`#button-output-delete-${outputId}`).click(function() { - - var pk = $(this).attr('pk'); - - launchModalForm( - `/build/${buildId}/delete-output/`, - { - reload: true, - data: { - output: pk - } - } - ); - }); -} - - /* * Unallocate stock against a particular build order * @@ -572,9 +464,6 @@ function loadBuildOutputTable(build_info, options={}) { $(table).find('.button-output-allocate').click(function() { var pk = $(this).attr('pk'); - // TODO - var todo = "Work out which stock items we need to allocate and launch the form"; - // Find the "allocation" sub-table associated with this output var subtable = $(`#output-sub-table-${pk}`); @@ -666,6 +555,7 @@ function loadBuildOutputTable(build_info, options={}) { row, { table: `#${sub_table_id}`, + parent_table: table, } ); } @@ -695,7 +585,6 @@ function loadBuildOutputTable(build_info, options={}) { setupBuildOutputButtonCallbacks(); $(table).bootstrapTable('expandAllRows'); - // $(table).bootstrapTable('collapseAllRows'); }, columns: [ { @@ -751,7 +640,17 @@ function loadBuildOutputTable(build_info, options={}) { } } ] - }); + }); + + // Enable the "allocate" button when the sub-table is exanded + $(table).on('expand-row.bs.table', function(detail, index, row) { + $(`#button-output-allocate-${row.pk}`).prop('disabled', false); + }); + + // Disable the "allocate" button when the sub-table is collapsed + $(table).on('collapse-row.bs.table', function(detail, index, row) { + $(`#button-output-allocate-${row.pk}`).prop('disabled', true); + }); } @@ -936,7 +835,10 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { name: 'build-allocation', uniqueId: 'sub_part', search: options.search || false, - onPostBody: setupCallbacks, + onPostBody: function(data) { + // Setup button callbacks + setupCallbacks(); + }, onLoadSuccess: function(tableData) { // Once the BOM data are loaded, request allocation data for this build output @@ -1031,9 +933,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { } else { console.log(`WARNING: Could not find progress bar for output ${outputId}`); } - - // Update the available actions for this build output - makeBuildOutputActionButtons(output, buildInfo, totalLines); } } ); @@ -1288,9 +1187,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { }, ] }); - - // Initialize the action buttons - makeBuildOutputActionButtons(output, buildInfo, 0); } diff --git a/InvenTree/templates/js/translated/helpers.js b/InvenTree/templates/js/translated/helpers.js index 164452952d..1bc15ea402 100644 --- a/InvenTree/templates/js/translated/helpers.js +++ b/InvenTree/templates/js/translated/helpers.js @@ -87,8 +87,10 @@ function select2Thumbnail(image) { } +/* + * Construct an 'icon badge' which floats to the right of an object + */ function makeIconBadge(icon, title) { - // Construct an 'icon badge' which floats to the right of an object var html = ``; @@ -96,8 +98,10 @@ function makeIconBadge(icon, title) { } +/* + * Construct an 'icon button' using the fontawesome set + */ function makeIconButton(icon, cls, pk, title, options={}) { - // Construct an 'icon button' using the fontawesome set var classes = `btn btn-default btn-glyph ${cls}`; From 542b4113a108f61c875577b5772afc6df14cee3f Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 21:37:10 +1100 Subject: [PATCH 17/26] Improvements for build output completion - Check if the output is fully allocated (throw error if not) - Reload tables after actions performed --- InvenTree/build/serializers.py | 4 +++ InvenTree/build/templates/build/detail.html | 4 +++ InvenTree/templates/js/translated/build.js | 32 ++++++++++++++++----- InvenTree/templates/js/translated/forms.js | 13 +++++++++ 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index cf7c8065fe..8f76f3e603 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -152,6 +152,10 @@ class BuildOutputSerializer(serializers.Serializer): if not output.is_building: raise ValidationError(_("This build output has already been completed")) + # The build output must have all tracked parts allocated + if not build.isFullyAllocated(output): + raise ValidationError(_("This build output is not fully allocated")) + return output class Meta: diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index e2bf2358de..fcd60c2edd 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -364,7 +364,11 @@ inventreeGet( outputs, { success: function() { + // Reload the "in progress" table $('#build-output-table').bootstrapTable('refresh'); + + // Reload the "completed" table + $('#build-stock-table').bootstrapTable('refresh'); } } ); diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 245904f5ca..40a5acee62 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -220,12 +220,12 @@ function completeBuildOutputs(build_id, outputs, options={}) { function renderBuildOutput(output, opts={}) { var pk = output.pk; - var quantity = ''; + var output_html = imageHoverIcon(output.part_detail.thumbnail); if (output.quantity == 1 && output.serial) { - quantity = `{% trans "Serial Number" %}: ${output.serial}`; + output_html += `{% trans "Serial Number" %}: ${output.serial}`; } else { - quantity = `{% trans "Quantity" %}: ${output.quantity}`; + output_html += `{% trans "Quantity" %}: ${output.quantity}`; } var buttons = `
    `; @@ -234,9 +234,21 @@ function completeBuildOutputs(build_id, outputs, options={}) { buttons += '
    '; + var field = constructField( + `outputs_output_${pk}`, + { + type: 'raw', + html: output_html, + }, + { + hideLabels: true, + } + ); + var html = ` - ${quantity} + ${field} + ${output.part_detail.full_name} ${buttons} `; @@ -253,7 +265,7 @@ function completeBuildOutputs(build_id, outputs, options={}) { var html = ` - + @@ -481,6 +493,9 @@ function loadBuildOutputTable(build_info, options={}) { rows, { output: pk, + success: function() { + $(table).bootstrapTable('refresh'); + } } ); } else { @@ -1296,10 +1311,13 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { remaining = 0; } - table_entries += renderBomItemRow(bom_item, remaining); + // We only care about entries which are not yet fully allocated + if (remaining > 0) { + table_entries += renderBomItemRow(bom_item, remaining); + } } - if (bom_items.length == 0) { + if (table_entries.length == 0) { showAlertDialog( '{% trans "Select Parts" %}', diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 2483263219..1bfe196286 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -1843,6 +1843,8 @@ function constructInput(name, parameters, options) { case 'candy': func = constructCandyInput; break; + case 'raw': + func = constructRawInput; default: // Unsupported field type! break; @@ -2086,6 +2088,17 @@ function constructCandyInput(name, parameters) { } +/* + * Construct a "raw" field input + * No actual field data! + */ +function constructRawInput(name, parameters) { + + return parameters.html; + +} + + /* * Construct a 'help text' div based on the field parameters * From 90593a187c90dc79c805a07270615c7c95340d7e Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 21:39:07 +1100 Subject: [PATCH 18/26] PEP fixes --- InvenTree/build/forms.py | 54 ---------------------------------------- InvenTree/build/views.py | 15 +++++++---- 2 files changed, 10 insertions(+), 59 deletions(-) diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index f385db7f9c..19bf3566dc 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -10,63 +10,9 @@ from django.utils.translation import ugettext_lazy as _ from django import forms from InvenTree.forms import HelperForm -from InvenTree.fields import RoundingDecimalFormField -from InvenTree.fields import DatePickerFormField - -from InvenTree.status_codes import StockStatus from .models import Build -from stock.models import StockLocation, StockItem - - -class EditBuildForm(HelperForm): - """ Form for editing a Build object. - """ - - field_prefix = { - 'reference': 'BO', - 'link': 'fa-link', - 'batch': 'fa-layer-group', - 'serial-numbers': 'fa-hashtag', - 'location': 'fa-map-marker-alt', - 'target_date': 'fa-calendar-alt', - } - - field_placeholder = { - 'reference': _('Build Order reference'), - 'target_date': _('Order target date'), - } - - target_date = DatePickerFormField( - label=_('Target Date'), - help_text=_('Target date for build completion. Build will be overdue after this date.') - ) - - quantity = RoundingDecimalFormField( - max_digits=10, decimal_places=5, - label=_('Quantity'), - help_text=_('Number of items to build') - ) - - class Meta: - model = Build - fields = [ - 'reference', - 'title', - 'part', - 'quantity', - 'batch', - 'target_date', - 'take_from', - 'destination', - 'parent', - 'sales_order', - 'link', - 'issued_by', - 'responsible', - ] - class BuildOutputCreateForm(HelperForm): """ diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 5499b48395..fd730b6a7e 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -12,16 +12,17 @@ from django.forms import HiddenInput from .models import Build from . import forms -from stock.models import StockLocation, StockItem +from stock.models import StockItem from InvenTree.views import AjaxUpdateView, AjaxDeleteView from InvenTree.views import InvenTreeRoleMixin from InvenTree.helpers import str2bool, extract_serial_numbers -from InvenTree.status_codes import BuildStatus, StockStatus +from InvenTree.status_codes import BuildStatus class BuildIndex(InvenTreeRoleMixin, ListView): - """ View for displaying list of Builds + """ + View for displaying list of Builds """ model = Build template_name = 'build/index.html' @@ -279,7 +280,9 @@ class BuildComplete(AjaxUpdateView): class BuildDetail(InvenTreeRoleMixin, DetailView): - """ Detail view of a single Build object. """ + """ + Detail view of a single Build object. + """ model = Build template_name = 'build/detail.html' @@ -307,7 +310,9 @@ class BuildDetail(InvenTreeRoleMixin, DetailView): class BuildDelete(AjaxDeleteView): - """ View to delete a build """ + """ + View to delete a build + """ model = Build ajax_template_name = 'build/delete_build.html' From 4a6c1e850f6739aa008e24869ea7828a64972fda Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 21:40:59 +1100 Subject: [PATCH 19/26] Cleanup unused form --- InvenTree/part/forms.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index f5d7d39266..f0f5a7c658 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -188,18 +188,6 @@ class EditPartParameterTemplateForm(HelperForm): ] -class EditPartParameterForm(HelperForm): - """ Form for editing a PartParameter object """ - - class Meta: - model = PartParameter - fields = [ - 'part', - 'template', - 'data' - ] - - class EditCategoryForm(HelperForm): """ Form for editing a PartCategory object """ From a97a918860d29f21c051769d2100c25fbb04351c Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 21:46:33 +1100 Subject: [PATCH 20/26] cut out the fat (removing dead code for outdated form views) --- InvenTree/order/forms.py | 21 ++------- InvenTree/part/forms.py | 2 +- InvenTree/stock/forms.py | 96 +++++++++++----------------------------- 3 files changed, 31 insertions(+), 88 deletions(-) diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 227109c46c..6f0bc43c46 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -9,7 +9,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ from InvenTree.forms import HelperForm -from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField +from InvenTree.fields import InvenTreeMoneyField from InvenTree.helpers import clean_decimal @@ -19,7 +19,6 @@ import part.models from .models import PurchaseOrder from .models import SalesOrder, SalesOrderLineItem -from .models import SalesOrderAllocation class IssuePurchaseOrderForm(HelperForm): @@ -81,6 +80,8 @@ class AllocateSerialsToSalesOrderForm(forms.Form): """ Form for assigning stock to a sales order, by serial number lookup + + TODO: Refactor this form / view to use the new API forms interface """ line = forms.ModelChoiceField( @@ -115,22 +116,6 @@ class AllocateSerialsToSalesOrderForm(forms.Form): ] -class EditSalesOrderAllocationForm(HelperForm): - """ - Form for editing a SalesOrderAllocation item - """ - - quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity')) - - class Meta: - model = SalesOrderAllocation - - fields = [ - 'line', - 'item', - 'quantity'] - - class OrderMatchItemForm(MatchItemForm): """ Override MatchItemForm fields """ diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index f0f5a7c658..ddcb78ac2a 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -18,7 +18,7 @@ import common.models from common.forms import MatchItemForm from .models import Part, PartCategory, PartRelated -from .models import PartParameterTemplate, PartParameter +from .models import PartParameterTemplate from .models import PartCategoryParameterTemplate from .models import PartSellPriceBreak, PartInternalPriceBreak diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 7e739306b0..96b6e0ba91 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -16,8 +16,6 @@ from InvenTree.forms import HelperForm from InvenTree.fields import RoundingDecimalFormField from InvenTree.fields import DatePickerFormField -from report.models import TestReport - from part.models import Part from .models import StockLocation, StockItem, StockItemTracking @@ -26,6 +24,8 @@ from .models import StockLocation, StockItem, StockItemTracking class AssignStockItemToCustomerForm(HelperForm): """ Form for manually assigning a StockItem to a Customer + + TODO: This could be a simple API driven form! """ class Meta: @@ -38,6 +38,8 @@ class AssignStockItemToCustomerForm(HelperForm): class ReturnStockItemForm(HelperForm): """ Form for manually returning a StockItem into stock + + TODO: This could be a simple API driven form! """ class Meta: @@ -48,7 +50,11 @@ class ReturnStockItemForm(HelperForm): class EditStockLocationForm(HelperForm): - """ Form for editing a StockLocation """ + """ + Form for editing a StockLocation + + TODO: Migrate this form to the modern API forms interface + """ class Meta: model = StockLocation @@ -63,6 +69,8 @@ class EditStockLocationForm(HelperForm): class ConvertStockItemForm(HelperForm): """ Form for converting a StockItem to a variant of its current part. + + TODO: Migrate this form to the modern API forms interface """ class Meta: @@ -73,7 +81,11 @@ class ConvertStockItemForm(HelperForm): class CreateStockItemForm(HelperForm): - """ Form for creating a new StockItem """ + """ + Form for creating a new StockItem + + TODO: Migrate this form to the modern API forms interface + """ expiry_date = DatePickerFormField( label=_('Expiry Date'), @@ -129,7 +141,11 @@ class CreateStockItemForm(HelperForm): class SerializeStockForm(HelperForm): - """ Form for serializing a StockItem. """ + """ + Form for serializing a StockItem. + + TODO: Migrate this form to the modern API forms interface + """ destination = TreeNodeChoiceField(queryset=StockLocation.objects.all(), label=_('Destination'), required=True, help_text=_('Destination for serialized stock (by default, will remain in current location)')) @@ -160,73 +176,11 @@ class SerializeStockForm(HelperForm): ] -class StockItemLabelSelectForm(HelperForm): - """ Form for selecting a label template for a StockItem """ - - label = forms.ChoiceField( - label=_('Label'), - help_text=_('Select test report template') - ) - - class Meta: - model = StockItem - fields = [ - 'label', - ] - - def get_label_choices(self, labels): - - choices = [] - - if len(labels) > 0: - for label in labels: - choices.append((label.pk, label)) - - return choices - - def __init__(self, labels, *args, **kwargs): - - super().__init__(*args, **kwargs) - - self.fields['label'].choices = self.get_label_choices(labels) - - -class TestReportFormatForm(HelperForm): - """ Form for selection a test report template """ - - class Meta: - model = StockItem - fields = [ - 'template', - ] - - def __init__(self, stock_item, *args, **kwargs): - self.stock_item = stock_item - - super().__init__(*args, **kwargs) - self.fields['template'].choices = self.get_template_choices() - - def get_template_choices(self): - """ - Generate a list of of TestReport options for the StockItem - """ - - choices = [] - - templates = TestReport.objects.filter(enabled=True) - - for template in templates: - if template.enabled and template.matches_stock_item(self.stock_item): - choices.append((template.pk, template)) - - return choices - - template = forms.ChoiceField(label=_('Template'), help_text=_('Select test report template')) - - class InstallStockForm(HelperForm): """ Form for manually installing a stock item into another stock item + + TODO: Migrate this form to the modern API forms interface """ part = forms.ModelChoiceField( @@ -275,6 +229,8 @@ class InstallStockForm(HelperForm): class UninstallStockForm(forms.ModelForm): """ Form for uninstalling a stock item which is installed in another item. + + TODO: Migrate this form to the modern API forms interface """ location = TreeNodeChoiceField(queryset=StockLocation.objects.all(), label=_('Location'), help_text=_('Destination location for uninstalled items')) @@ -301,6 +257,8 @@ class EditStockItemForm(HelperForm): location - Must be updated in a 'move' transaction quantity - Must be updated in a 'stocktake' transaction part - Cannot be edited after creation + + TODO: Migrate this form to the modern API forms interface """ expiry_date = DatePickerFormField( From 02f4f0fd156d6c6e259aa8665cbf5f044cda6963 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 21:50:18 +1100 Subject: [PATCH 21/26] js linting fixes --- InvenTree/templates/js/dynamic/inventree.js | 2 +- InvenTree/templates/js/dynamic/nav.js | 19 +++++++++++-------- InvenTree/templates/js/translated/build.js | 4 ++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/InvenTree/templates/js/dynamic/inventree.js b/InvenTree/templates/js/dynamic/inventree.js index d06399c61e..7513b4863a 100644 --- a/InvenTree/templates/js/dynamic/inventree.js +++ b/InvenTree/templates/js/dynamic/inventree.js @@ -294,4 +294,4 @@ function loadBrandIcon(element, name) { // Convenience function to determine if an element exists $.fn.exists = function() { return this.length !== 0; -} +}; diff --git a/InvenTree/templates/js/dynamic/nav.js b/InvenTree/templates/js/dynamic/nav.js index fb26c841c8..d14c4799cd 100644 --- a/InvenTree/templates/js/dynamic/nav.js +++ b/InvenTree/templates/js/dynamic/nav.js @@ -3,6 +3,9 @@ /* exported attachNavCallbacks, + enableNavBar, + initNavTree, + loadTree, onPanelLoad, */ @@ -132,7 +135,7 @@ function loadTree(url, tree, options={}) { data = options.data; } - var key = "inventree-sidenav-items-"; + var key = 'inventree-sidenav-items-'; if (options.name) { key += options.name; @@ -143,7 +146,7 @@ function loadTree(url, tree, options={}) { type: 'get', dataType: 'json', data: data, - success: function (response) { + success: function(response) { if (response.tree) { $(tree).treeview({ data: response.tree, @@ -152,7 +155,7 @@ function loadTree(url, tree, options={}) { }); if (localStorage.getItem(key)) { - var saved_exp = localStorage.getItem(key).split(","); + var saved_exp = localStorage.getItem(key).split(','); // Automatically expand the desired notes for (var q = 0; q < saved_exp.length; q++) { @@ -177,8 +180,8 @@ function loadTree(url, tree, options={}) { }); } }, - error: function (xhr, ajaxOptions, thrownError) { - //TODO + error: function(xhr, ajaxOptions, thrownError) { + // TODO } }); } @@ -292,9 +295,9 @@ function enableNavbar(options) { // Extract the saved width for this element $(navId).animate({ - width: '45px', + 'width': '45px', 'min-width': '45px', - display: 'block', + 'display': 'block', }, 50, function() { // Make the navbar resizable @@ -362,4 +365,4 @@ function enableNavbar(options) { } }); } -} \ No newline at end of file +} diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 40a5acee62..0c291dd8da 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -144,7 +144,7 @@ function makeBuildOutputButtons(output_id, build_info, options={}) { 'button-output-complete', output_id, '{% trans "Complete build output" %}', - ) + ); // Add a button to "delete" this build output html += makeIconButton( @@ -344,7 +344,7 @@ function completeBuildOutputs(build_id, outputs, options={}) { } } } - ) + ); } }); } From 7c5c1b669614ecf53e08d62e831d148ca96e2e98 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 21:55:33 +1100 Subject: [PATCH 22/26] typo fix --- InvenTree/templates/js/dynamic/nav.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/dynamic/nav.js b/InvenTree/templates/js/dynamic/nav.js index d14c4799cd..ddf3cb8c12 100644 --- a/InvenTree/templates/js/dynamic/nav.js +++ b/InvenTree/templates/js/dynamic/nav.js @@ -3,7 +3,7 @@ /* exported attachNavCallbacks, - enableNavBar, + enableNavbar, initNavTree, loadTree, onPanelLoad, From 6ebb956c8dac903a72dba0db7a80c13c5c796bd2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 22:32:03 +1100 Subject: [PATCH 23/26] Fix generation of API documentation - Can't use get_object_or_404 in API views! --- InvenTree/build/api.py | 20 ++++++++++++++++---- InvenTree/order/api.py | 7 +++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index cfc2cab16f..819ffed1b6 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -5,7 +5,6 @@ JSON API for the Build app # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.shortcuts import get_object_or_404 from django.conf.urls import url, include from rest_framework import filters, generics @@ -203,7 +202,12 @@ class BuildUnallocate(generics.CreateAPIView): def get_serializer_context(self): ctx = super().get_serializer_context() - ctx['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None)) + + try: + ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None)) + except: + pass + ctx['request'] = self.request return ctx @@ -222,7 +226,11 @@ class BuildComplete(generics.CreateAPIView): ctx = super().get_serializer_context() ctx['request'] = self.request - ctx['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None)) + + try: + ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None)) + except: + pass return ctx @@ -250,7 +258,11 @@ class BuildAllocate(generics.CreateAPIView): context = super().get_serializer_context() - context['build'] = get_object_or_404(Build, pk=self.kwargs.get('pk', None)) + try: + context['build'] = Build.objects.get(pk=self.kwargs.get('pk', None)) + except: + pass + context['request'] = self.request return context diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 1b294d3e3e..f4ebff4dfb 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -7,7 +7,6 @@ from __future__ import unicode_literals from django.conf.urls import url, include from django.db.models import Q, F -from django.shortcuts import get_object_or_404 from django_filters import rest_framework as rest_filters from rest_framework import generics @@ -235,7 +234,11 @@ class POReceive(generics.CreateAPIView): context = super().get_serializer_context() # Pass the purchase order through to the serializer for validation - context['order'] = get_object_or_404(PurchaseOrder, pk=self.kwargs.get('pk', None)) + try: + context['order'] = PurchaseOrder.objects.get(pk=self.kwargs.get('pk', None)) + except: + pass + context['request'] = self.request return context From 83faaa2da3b9ef09e993cc9827bd2e676ff3328a Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 22:57:07 +1100 Subject: [PATCH 24/26] unit test fixes --- InvenTree/build/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index e14f52ee04..e2b6448f2f 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -60,7 +60,7 @@ class BuildCompleteTest(BuildAPITest): self.post( reverse('api-build-complete', kwargs={'pk': 99999}), {}, - expected_code=404 + expected_code=400 ) data = self.post(self.url, {}, expected_code=400).data From fb6f15f462a32bba9c77e66fa5c1d45957a43777 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 17 Oct 2021 22:57:15 +1100 Subject: [PATCH 25/26] Delete a bunch of old template files --- .../templates/build/edit_build_item.html | 10 ---- .../templates/common/delete_currency.html | 7 --- .../stock/templates/stock/stock_adjust.html | 50 ------------------- .../stock/templates/stock/stock_move.html | 1 - InvenTree/templates/attachment_delete.html | 7 --- InvenTree/templates/collapse.html | 23 --------- InvenTree/templates/collapse_index.html | 19 ------- InvenTree/templates/required_part_table.html | 23 --------- InvenTree/templates/slide.html | 3 -- 9 files changed, 143 deletions(-) delete mode 100644 InvenTree/build/templates/build/edit_build_item.html delete mode 100644 InvenTree/common/templates/common/delete_currency.html delete mode 100644 InvenTree/stock/templates/stock/stock_adjust.html delete mode 100644 InvenTree/stock/templates/stock/stock_move.html delete mode 100644 InvenTree/templates/attachment_delete.html delete mode 100644 InvenTree/templates/collapse.html delete mode 100644 InvenTree/templates/collapse_index.html delete mode 100644 InvenTree/templates/required_part_table.html delete mode 100644 InvenTree/templates/slide.html diff --git a/InvenTree/build/templates/build/edit_build_item.html b/InvenTree/build/templates/build/edit_build_item.html deleted file mode 100644 index 99cad71ba2..0000000000 --- a/InvenTree/build/templates/build/edit_build_item.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "modal_form.html" %} -{% load i18n %} - -{% block pre_form_content %} -
    -

    - {% trans "Alter the quantity of stock allocated to the build output" %} -

    -
    -{% endblock %} \ No newline at end of file diff --git a/InvenTree/common/templates/common/delete_currency.html b/InvenTree/common/templates/common/delete_currency.html deleted file mode 100644 index 9dfa320668..0000000000 --- a/InvenTree/common/templates/common/delete_currency.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "modal_delete_form.html" %} - -{% block pre_form_content %} - -Are you sure you wish to delete this currency? - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/stock_adjust.html b/InvenTree/stock/templates/stock/stock_adjust.html deleted file mode 100644 index 60a9ec2658..0000000000 --- a/InvenTree/stock/templates/stock/stock_adjust.html +++ /dev/null @@ -1,50 +0,0 @@ -{% load i18n %} -{% load inventree_extras %} - -{% block pre_form_content %} - -{% endblock %} - -
    - {% csrf_token %} - {% load crispy_forms_tags %} - - - -
    {% trans "Output" %}{% trans "Output" %}
    - - - - - {% if edit_quantity %} - - {% endif %} - - - {% for item in stock_items %} - - - - - - - - {% endfor %} -
    {% trans "Stock Item" %}{% trans "Location" %}{% trans "Quantity" %}{{ stock_action_title }}
    {% include "hover_image.html" with image=item.part.image hover=True %} - {{ item.part.full_name }} {{ item.part.description }}{{ item.location.pathstring }}{% decimal item.quantity %} - {% if edit_quantity %} - - {% if item.error %} -
    {{ item.error }} - {% endif %} - {% else %} - - {% endif %} -
    - - {% crispy form %} - - \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_move.html deleted file mode 100644 index c7de8c74b2..0000000000 --- a/InvenTree/stock/templates/stock/stock_move.html +++ /dev/null @@ -1 +0,0 @@ -{% extends "modal_form.html" %} \ No newline at end of file diff --git a/InvenTree/templates/attachment_delete.html b/InvenTree/templates/attachment_delete.html deleted file mode 100644 index 4ee7f03cb1..0000000000 --- a/InvenTree/templates/attachment_delete.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "modal_delete_form.html" %} -{% load i18n %} - -{% block pre_form_content %} -{% trans "Are you sure you want to delete this attachment?" %} -
    -{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/collapse.html b/InvenTree/templates/collapse.html deleted file mode 100644 index 5624f34094..0000000000 --- a/InvenTree/templates/collapse.html +++ /dev/null @@ -1,23 +0,0 @@ -{% block collapse_preamble %} -{% endblock %} -
    -
    -
    -
    - - {% block collapse_heading %} - {% endblock %} -
    -
    -
    -
    - {% block collapse_content %} - {% endblock %} -
    -
    -
    -
    \ No newline at end of file diff --git a/InvenTree/templates/collapse_index.html b/InvenTree/templates/collapse_index.html deleted file mode 100644 index 6e918d7217..0000000000 --- a/InvenTree/templates/collapse_index.html +++ /dev/null @@ -1,19 +0,0 @@ -{% block collapse_preamble %} -{% endblock %} -
    -
    -
    - - {% block collapse_heading %} - {% endblock %} -
    -
    -
    - {% block collapse_content %} - {% endblock %} -
    -
    -
    -
    \ No newline at end of file diff --git a/InvenTree/templates/required_part_table.html b/InvenTree/templates/required_part_table.html deleted file mode 100644 index a1e26e2894..0000000000 --- a/InvenTree/templates/required_part_table.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - {% for part in parts %} - - - - - - - {{ part.net_stock }} - - {% endfor %} -
    PartDescriptionIn StockOn OrderAlloctedNet Stock
    - {% include "hover_image.html" with image=part.image hover=True %} - {{ part.full_name }} - {{ part.description }}{{ part.total_stock }}{{ part.on_order }}{{ part.allocation_count }}
    \ No newline at end of file diff --git a/InvenTree/templates/slide.html b/InvenTree/templates/slide.html deleted file mode 100644 index edd39e75a2..0000000000 --- a/InvenTree/templates/slide.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - -
    \ No newline at end of file From e04dfa0681af97ceec0169011fecef1ee501e6ca Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 18 Oct 2021 08:58:04 +1100 Subject: [PATCH 26/26] Unit test fix --- InvenTree/build/tests.py | 49 +--------------------------------------- 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index 7b2568b1c7..7afd078ce9 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -15,7 +15,7 @@ from datetime import datetime, timedelta from .models import Build from stock.models import StockItem -from InvenTree.status_codes import BuildStatus, StockStatus +from InvenTree.status_codes import BuildStatus class BuildTestSimple(TestCase): @@ -252,53 +252,6 @@ class TestBuildViews(TestCase): self.assertIn(build.title, content) - def test_build_output_complete(self): - """ - Test the build output completion form - """ - - # Firstly, check that the build cannot be completed! - self.assertFalse(self.build.can_complete) - - url = reverse('build-output-complete', 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, valid location - response = self.client.post( - url, - { - 'confirm': 1, - 'confirm_incomplete': 1, - 'location': 1, - 'output': self.output.pk, - 'stock_status': StockStatus.DAMAGED - }, - HTTP_X_REQUESTED_WITH='XMLHttpRequest' - ) - - self.assertEqual(response.status_code, 200) - - data = json.loads(response.content) - - self.assertTrue(data['form_valid']) - - # Now the build should be able to be completed - self.build.refresh_from_db() - self.assertTrue(self.build.can_complete) - - # Test with confirmation, invalid location - response = self.client.post(url, {'confirm': 1, 'location': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - data = json.loads(response.content) - self.assertFalse(data['form_valid']) - def test_build_cancel(self): """ Test the build cancellation form """