Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Build] Create child builds #7941

Merged
merged 9 commits into from
Aug 21, 2024
Merged
4 changes: 4 additions & 0 deletions docs/docs/build/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <span class='fas fa-tools'></span> icon on the build detail page, the `Complete Build` form will be displayed.
Expand Down
7 changes: 6 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
5 changes: 3 additions & 2 deletions src/backend/InvenTree/build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,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)

Expand Down
39 changes: 37 additions & 2 deletions src/backend/InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
from InvenTree.serializers import InvenTreeModelSerializer, UserSerializer

import InvenTree.helpers
import InvenTree.tasks
from InvenTree.serializers import InvenTreeDecimalField, NotesFieldMixin
from stock.status_codes import StockStatus

from stock.generators import generate_batch_code
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
Expand Down Expand Up @@ -77,6 +79,9 @@ class Meta:
'responsible_detail',
'priority',
'level',

# Additional fields used only for build order creation
'create_child_builds',
]

read_only_fields = [
Expand All @@ -88,6 +93,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)
Expand All @@ -112,6 +119,12 @@ class Meta:

project_code_detail = ProjectCodeSerializer(source='project_code', many=False, read_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):
"""Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible.
Expand All @@ -136,13 +149,19 @@ 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)
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"""
Expand All @@ -151,6 +170,22 @@ def validate_reference(self, reference):

return reference

def create(self, validated_data):
"""Save the Build object."""

build_order = super().create(validated_data)

create_child_builds = self.validated_data.pop('create_child_builds', False)

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,
)

return build_order


class BuildOutputSerializer(serializers.Serializer):
"""Serializer for a "BuildOutput".
Expand Down
36 changes: 36 additions & 0 deletions src/backend/InvenTree/build/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,42 @@ 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

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):
"""Notify appropriate users that a Build has just become 'overdue'"""
targets = []
Expand Down
75 changes: 74 additions & 1 deletion src/backend/InvenTree/build/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions src/backend/InvenTree/templates/js/translated/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 7 additions & 1 deletion src/frontend/src/forms/BuildForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function useBuildOrderFields({
const globalSettings = useGlobalSettingsState();

return useMemo(() => {
return {
let fields: ApiFormFieldSet = {
reference: {},
part: {
disabled: !create,
Expand Down Expand Up @@ -119,6 +119,12 @@ export function useBuildOrderFields({
}
}
};

if (create) {
fields.create_child_builds = {};
}

return fields;
}, [create, destination, batchCode, globalSettings]);
}

Expand Down
Loading