mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[Build] Create child builds (#7941)
* Add "create_child_builds" field to BuildOrder serializer - only when creating a new order - write only field * Update serializer field * Add placeholder task for creating child build orders * Add field to PUI forms * Auto-create build orders as required * Bump API vresion * Add documentation * Update unit tests
This commit is contained in:
parent
7709d8df70
commit
8474b7bf4c
4
docs/docs/build/build.md
vendored
4
docs/docs/build/build.md
vendored
@ -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.
|
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
|
## 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.
|
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.
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
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
|
v243 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7940
|
||||||
- Expose "ancestor" filter to the BuildOrder API
|
- Expose "ancestor" filter to the BuildOrder API
|
||||||
|
|
||||||
|
@ -253,11 +253,12 @@ class BuildList(DataExportViewMixin, BuildMixin, ListCreateAPI):
|
|||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Add extra context information to the endpoint serializer."""
|
"""Add extra context information to the endpoint serializer."""
|
||||||
try:
|
try:
|
||||||
part_detail = str2bool(self.request.GET.get('part_detail', None))
|
part_detail = str2bool(self.request.GET.get('part_detail', True))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
part_detail = None
|
part_detail = True
|
||||||
|
|
||||||
kwargs['part_detail'] = part_detail
|
kwargs['part_detail'] = part_detail
|
||||||
|
kwargs['create'] = True
|
||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ from rest_framework.serializers import ValidationError
|
|||||||
from InvenTree.serializers import InvenTreeModelSerializer, UserSerializer
|
from InvenTree.serializers import InvenTreeModelSerializer, UserSerializer
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.tasks
|
||||||
from InvenTree.serializers import InvenTreeDecimalField, NotesFieldMixin
|
from InvenTree.serializers import InvenTreeDecimalField, NotesFieldMixin
|
||||||
from stock.status_codes import StockStatus
|
from stock.status_codes import StockStatus
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ from stock.generators import generate_batch_code
|
|||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
from stock.serializers import StockItemSerializerBrief, LocationBriefSerializer
|
from stock.serializers import StockItemSerializerBrief, LocationBriefSerializer
|
||||||
|
|
||||||
|
import build.tasks
|
||||||
import common.models
|
import common.models
|
||||||
from common.serializers import ProjectCodeSerializer
|
from common.serializers import ProjectCodeSerializer
|
||||||
from common.settings import get_global_setting
|
from common.settings import get_global_setting
|
||||||
@ -77,6 +79,9 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
|
|||||||
'responsible_detail',
|
'responsible_detail',
|
||||||
'priority',
|
'priority',
|
||||||
'level',
|
'level',
|
||||||
|
|
||||||
|
# Additional fields used only for build order creation
|
||||||
|
'create_child_builds',
|
||||||
]
|
]
|
||||||
|
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
@ -88,6 +93,8 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
|
|||||||
'level',
|
'level',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
reference = serializers.CharField(required=True)
|
||||||
|
|
||||||
level = serializers.IntegerField(label=_('Build Level'), read_only=True)
|
level = serializers.IntegerField(label=_('Build Level'), read_only=True)
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
@ -112,6 +119,12 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
|
|||||||
|
|
||||||
project_code_detail = ProjectCodeSerializer(source='project_code', many=False, read_only=True)
|
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
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible.
|
"""Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible.
|
||||||
@ -136,13 +149,19 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Determine if extra serializer fields are required"""
|
"""Determine if extra serializer fields are required"""
|
||||||
part_detail = kwargs.pop('part_detail', True)
|
part_detail = kwargs.pop('part_detail', True)
|
||||||
|
create = kwargs.pop('create', False)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
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)
|
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):
|
def validate_reference(self, reference):
|
||||||
"""Custom validation for the Build reference field"""
|
"""Custom validation for the Build reference field"""
|
||||||
@ -151,6 +170,22 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
|
|||||||
|
|
||||||
return 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):
|
class BuildOutputSerializer(serializers.Serializer):
|
||||||
"""Serializer for a "BuildOutput".
|
"""Serializer for a "BuildOutput".
|
||||||
|
@ -188,6 +188,42 @@ def check_build_stock(build: build.models.Build):
|
|||||||
InvenTree.email.send_email(subject, '', recipients, html_message=html_message)
|
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):
|
def notify_overdue_build_order(bo: build.models.Build):
|
||||||
"""Notify appropriate users that a Build has just become 'overdue'"""
|
"""Notify appropriate users that a Build has just become 'overdue'"""
|
||||||
targets = []
|
targets = []
|
||||||
|
@ -6,7 +6,7 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from part.models import Part
|
from part.models import Part, BomItem
|
||||||
from build.models import Build, BuildItem
|
from build.models import Build, BuildItem
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
@ -605,6 +605,79 @@ class BuildTest(BuildAPITest):
|
|||||||
self.assertEqual(build.reference, row['Reference'])
|
self.assertEqual(build.reference, row['Reference'])
|
||||||
self.assertEqual(build.title, row['Description'])
|
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):
|
class BuildAllocationTest(BuildAPITest):
|
||||||
"""Unit tests for allocation of stock items against a build order.
|
"""Unit tests for allocation of stock items against a build order.
|
||||||
|
@ -154,6 +154,9 @@ function newBuildOrder(options={}) {
|
|||||||
|
|
||||||
var fields = buildFormFields();
|
var fields = buildFormFields();
|
||||||
|
|
||||||
|
// Add "create_child_builds" field
|
||||||
|
fields.create_child_builds = {};
|
||||||
|
|
||||||
// Specify the target part
|
// Specify the target part
|
||||||
if (options.part) {
|
if (options.part) {
|
||||||
fields.part.value = options.part;
|
fields.part.value = options.part;
|
||||||
|
@ -47,7 +47,7 @@ export function useBuildOrderFields({
|
|||||||
const globalSettings = useGlobalSettingsState();
|
const globalSettings = useGlobalSettingsState();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return {
|
let fields: ApiFormFieldSet = {
|
||||||
reference: {},
|
reference: {},
|
||||||
part: {
|
part: {
|
||||||
disabled: !create,
|
disabled: !create,
|
||||||
@ -119,6 +119,12 @@ export function useBuildOrderFields({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (create) {
|
||||||
|
fields.create_child_builds = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
}, [create, destination, batchCode, globalSettings]);
|
}, [create, destination, batchCode, globalSettings]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user