From 100ceabdc3574e27e7d41c167958a2119ff79f8f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 21 Aug 2024 00:10:51 +0000 Subject: [PATCH 1/8] Add "create_child_builds" field to BuildOrder serializer - only when creating a new order - write only field --- src/backend/InvenTree/build/api.py | 5 +++-- src/backend/InvenTree/build/serializers.py | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index d456b2d78b1a..fa37a2d67269 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -280,11 +280,12 @@ def filter_queryset(self, queryset): def get_serializer(self, *args, **kwargs): """Add extra context information to the endpoint serializer.""" try: - part_detail = str2bool(self.request.GET.get('part_detail', None)) + part_detail = str2bool(self.request.GET.get('part_detail', True)) except AttributeError: - part_detail = None + part_detail = True kwargs['part_detail'] = part_detail + kwargs['create'] = True return self.serializer_class(*args, **kwargs) diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index 66fd69309dce..70abacd563f2 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -77,6 +77,9 @@ class Meta: 'responsible_detail', 'priority', 'level', + + # Additional fields used only for build order creation + 'create_child_builds', ] read_only_fields = [ @@ -112,6 +115,8 @@ class Meta: project_code_detail = ProjectCodeSerializer(source='project_code', many=False, read_only=True) + create_child_builds = serializers.BooleanField(default=False, required=False, label=_('Create Child Builds'), write_only=True) + @staticmethod def annotate_queryset(queryset): """Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible. @@ -136,10 +141,14 @@ def annotate_queryset(queryset): def __init__(self, *args, **kwargs): """Determine if extra serializer fields are required""" part_detail = kwargs.pop('part_detail', True) + create = kwargs.pop('create', False) super().__init__(*args, **kwargs) - if part_detail is not True: + if not create: + self.fields.pop('create_child_builds', None) + + if not part_detail: self.fields.pop('part_detail', None) reference = serializers.CharField(required=True) @@ -151,6 +160,15 @@ def validate_reference(self, reference): return reference + def save(self): + """Save the Build object.""" + + data = self.validated_data + + print("data:", data) + + super().save() + class BuildOutputSerializer(serializers.Serializer): """Serializer for a "BuildOutput". From 592d8a3b3e09794fe47dd5e70c4289951436abcd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 21 Aug 2024 00:24:52 +0000 Subject: [PATCH 2/8] Update serializer field --- src/backend/InvenTree/build/serializers.py | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index 70abacd563f2..481d56099939 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -91,6 +91,8 @@ class Meta: 'level', ] + reference = serializers.CharField(required=True) + level = serializers.IntegerField(label=_('Build Level'), read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True) @@ -115,7 +117,11 @@ class Meta: project_code_detail = ProjectCodeSerializer(source='project_code', many=False, read_only=True) - create_child_builds = serializers.BooleanField(default=False, required=False, label=_('Create Child Builds'), write_only=True) + create_child_builds = serializers.BooleanField( + default=False, required=False, write_only=True, + label=_('Create Child Builds'), + help_text=_('Automatically generate child build orders'), + ) @staticmethod def annotate_queryset(queryset): @@ -151,7 +157,9 @@ def __init__(self, *args, **kwargs): if not part_detail: self.fields.pop('part_detail', None) - reference = serializers.CharField(required=True) + def skip_create_fields(self): + """Return a list of fields to skip during model creation.""" + return ['create_child_builds'] def validate_reference(self, reference): """Custom validation for the Build reference field""" @@ -160,14 +168,17 @@ def validate_reference(self, reference): return reference - def save(self): + def create(self, validated_data): """Save the Build object.""" - data = self.validated_data + create_child_builds = validated_data.pop('create_child_builds', False) + + print("data:", validated_data) - print("data:", data) + build = super().create(validated_data) - super().save() + # TODO: Implement child build order creation + return build class BuildOutputSerializer(serializers.Serializer): From f45f0f24e3817be4c480c78df09fa30aae191238 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 21 Aug 2024 00:40:32 +0000 Subject: [PATCH 3/8] Add placeholder task for creating child build orders --- src/backend/InvenTree/build/serializers.py | 16 +++++++++++----- src/backend/InvenTree/build/tasks.py | 16 ++++++++++++++++ .../InvenTree/templates/js/translated/build.js | 3 +++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index 481d56099939..ade07fbdf9c2 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -18,6 +18,7 @@ from InvenTree.serializers import InvenTreeModelSerializer, UserSerializer import InvenTree.helpers +import InvenTree.tasks from InvenTree.serializers import InvenTreeDecimalField, NotesFieldMixin from stock.status_codes import StockStatus @@ -25,6 +26,7 @@ from stock.models import StockItem, StockLocation from stock.serializers import StockItemSerializerBrief, LocationBriefSerializer +import build.tasks import common.models from common.serializers import ProjectCodeSerializer from common.settings import get_global_setting @@ -171,14 +173,18 @@ def validate_reference(self, reference): def create(self, validated_data): """Save the Build object.""" - create_child_builds = validated_data.pop('create_child_builds', False) + build_order = super().create(validated_data) - print("data:", validated_data) + create_child_builds = self.validated_data.pop('create_child_builds', False) - build = super().create(validated_data) + if create_child_builds: + # Pass child build creation off to the background thread + InvenTree.tasks.offload_task( + build.tasks.create_child_builds, + build_order.pk, + ) - # TODO: Implement child build order creation - return build + return build_order class BuildOutputSerializer(serializers.Serializer): diff --git a/src/backend/InvenTree/build/tasks.py b/src/backend/InvenTree/build/tasks.py index 82828d642d4e..348cc51a43c5 100644 --- a/src/backend/InvenTree/build/tasks.py +++ b/src/backend/InvenTree/build/tasks.py @@ -188,6 +188,22 @@ def check_build_stock(build: build.models.Build): InvenTree.email.send_email(subject, '', recipients, html_message=html_message) +def create_child_builds(build_id: int) -> None: + """Create child build orders for a given parent build. + + - Will create a build order for each assembly part in the BOM + - Runs recursively, also creating child builds for each sub-assembly part + """ + + try: + build_order = build.models.Build.objects.get(pk=build_id) + except (Build.DoesNotExist, ValueError): + return + + print("creating child builds for:", build_order) + + + def notify_overdue_build_order(bo: build.models.Build): """Notify appropriate users that a Build has just become 'overdue'""" targets = [] diff --git a/src/backend/InvenTree/templates/js/translated/build.js b/src/backend/InvenTree/templates/js/translated/build.js index a21e140b2fe8..ba5b7d58a4bc 100644 --- a/src/backend/InvenTree/templates/js/translated/build.js +++ b/src/backend/InvenTree/templates/js/translated/build.js @@ -154,6 +154,9 @@ function newBuildOrder(options={}) { var fields = buildFormFields(); + // Add "create_child_builds" field + fields.create_child_builds = {}; + // Specify the target part if (options.part) { fields.part.value = options.part; From 3b1d9125f44acc8cd96b70675cd609953a6db1fd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 21 Aug 2024 02:37:29 +0000 Subject: [PATCH 4/8] Add field to PUI forms --- src/frontend/src/forms/BuildForms.tsx | 8 +++++++- src/frontend/src/tables/build/BuildOrderTable.tsx | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/forms/BuildForms.tsx b/src/frontend/src/forms/BuildForms.tsx index 3e8d86adb14e..1590c4a55b85 100644 --- a/src/frontend/src/forms/BuildForms.tsx +++ b/src/frontend/src/forms/BuildForms.tsx @@ -47,7 +47,7 @@ export function useBuildOrderFields({ const globalSettings = useGlobalSettingsState(); return useMemo(() => { - return { + let fields: ApiFormFieldSet = { reference: {}, part: { disabled: !create, @@ -119,6 +119,12 @@ export function useBuildOrderFields({ } } }; + + if (create) { + fields.create_child_builds = {}; + } + + return fields; }, [create, destination, batchCode, globalSettings]); } diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 737419d55121..1db82dc99f81 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -13,7 +13,6 @@ import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; -import { TableColumn } from '../Column'; import { CreationDateColumn, DateColumn, From f11a2bf9178693a9cec20ac809f42d75b04cdc8e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 21 Aug 2024 02:42:55 +0000 Subject: [PATCH 5/8] Auto-create build orders as required --- src/backend/InvenTree/build/tasks.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/build/tasks.py b/src/backend/InvenTree/build/tasks.py index 348cc51a43c5..5c95cfe70e99 100644 --- a/src/backend/InvenTree/build/tasks.py +++ b/src/backend/InvenTree/build/tasks.py @@ -200,8 +200,28 @@ def create_child_builds(build_id: int) -> None: except (Build.DoesNotExist, ValueError): return - print("creating child builds for:", build_order) - + assembly_items = build_order.part.get_bom_items().filter(sub_part__assembly=True) + + for item in assembly_items: + quantity = item.quantity * build_order.quantity + + sub_order = build.models.Build.objects.create( + part=item.sub_part, + quantity=quantity, + title=build_order.title, + batch=build_order.batch, + parent=build_order, + target_date=build_order.target_date, + sales_order=build_order.sales_order, + issued_by=build_order.issued_by, + responsible=build_order.responsible, + ) + + # Offload the child build order creation to the background task queue + InvenTree.tasks.offload_task( + create_child_builds, + sub_order.pk + ) def notify_overdue_build_order(bo: build.models.Build): From 94e1f53de1e3829f5ed2cd1ae65be66ec5c0fab0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 21 Aug 2024 02:45:08 +0000 Subject: [PATCH 6/8] Bump API vresion --- src/backend/InvenTree/InvenTree/api_version.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index b49cea36e1d2..599fb56daf7e 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,18 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 243 +INVENTREE_API_VERSION = 244 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v244 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7941 + - Adds "create_child_builds" field to the Build API + - Write-only field to create child builds from the API + - Only available when creating a new build order + v243 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7940 - Expose "ancestor" filter to the BuildOrder API From b9775b916c58cfcadecb760b848561fcd95fce14 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 21 Aug 2024 05:28:52 +0000 Subject: [PATCH 7/8] Add documentation --- docs/docs/build/build.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/docs/build/build.md b/docs/docs/build/build.md index 09c0add44f3b..92d5a8671a92 100644 --- a/docs/docs/build/build.md +++ b/docs/docs/build/build.md @@ -221,6 +221,10 @@ To create a build order for your part, you have two options: Fill-out the form as required, then click the "Submit" button to create the build. +### Create Child Builds + +When creating a new build order, you have the option to automatically generate build orders for any subassembly parts. This can be useful to create a complete tree of build orders for a complex assembly. *However*, it must be noted that any build orders created for subassemblies will use the default BOM quantity for that part. Any child build orders created in this manner must be manually reviewed, to ensure that the correct quantity is being built as per your production requirements. + ## Complete Build Order To complete a build, click on icon on the build detail page, the `Complete Build` form will be displayed. From 5d789185d0d1e795987aee0374baf38b7b7a7949 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 21 Aug 2024 05:54:57 +0000 Subject: [PATCH 8/8] Update unit tests --- src/backend/InvenTree/build/test_api.py | 75 ++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py index 518d4c40982e..9183e1b43650 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -6,7 +6,7 @@ from rest_framework import status -from part.models import Part +from part.models import Part, BomItem from build.models import Build, BuildItem from stock.models import StockItem @@ -605,6 +605,79 @@ def test_download_build_orders(self): self.assertEqual(build.reference, row['Reference']) self.assertEqual(build.title, row['Description']) + def test_create(self): + """Test creation of new build orders via the API.""" + + url = reverse('api-build-list') + + # First, we'll create a tree of part assemblies + part_a = Part.objects.create(name="Part A", description="Part A description", assembly=True) + part_b = Part.objects.create(name="Part B", description="Part B description", assembly=True) + part_c = Part.objects.create(name="Part C", description="Part C description", assembly=True) + + # Create a BOM for Part A + BomItem.objects.create( + part=part_a, + sub_part=part_b, + quantity=5, + ) + + # Create a BOM for Part B + BomItem.objects.create( + part=part_b, + sub_part=part_c, + quantity=7 + ) + + n = Build.objects.count() + + # Create a build order for Part A, with a quantity of 10 + response = self.post( + url, + { + 'reference': 'BO-9876', + 'part': part_a.pk, + 'quantity': 10, + 'title': 'A build', + }, + expected_code=201 + ) + + self.assertEqual(n + 1, Build.objects.count()) + + bo = Build.objects.get(pk=response.data['pk']) + + self.assertEqual(bo.children.count(), 0) + + # Create a build order for Part A, and auto-create child builds + response = self.post( + url, + { + 'reference': 'BO-9875', + 'part': part_a.pk, + 'quantity': 15, + 'title': 'A build - with childs', + 'create_child_builds': True, + } + ) + + # An addition 1 + 2 builds should have been created + self.assertEqual(n + 4, Build.objects.count()) + + bo = Build.objects.get(pk=response.data['pk']) + + # One build has a direct child + self.assertEqual(bo.children.count(), 1) + child = bo.children.first() + self.assertEqual(child.part.pk, part_b.pk) + self.assertEqual(child.quantity, 75) + + # And there should be a second-level child build too + self.assertEqual(child.children.count(), 1) + child = child.children.first() + self.assertEqual(child.part.pk, part_c.pk) + self.assertEqual(child.quantity, 7 * 5 * 15) + class BuildAllocationTest(BuildAPITest): """Unit tests for allocation of stock items against a build order.