mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 12:35:46 +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:
@ -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
|
||||
|
||||
|
@ -253,11 +253,12 @@ class BuildList(DataExportViewMixin, BuildMixin, ListCreateAPI):
|
||||
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)
|
||||
|
||||
|
@ -18,6 +18,7 @@ from rest_framework.serializers import ValidationError
|
||||
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.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
|
||||
@ -77,6 +79,9 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
|
||||
'responsible_detail',
|
||||
'priority',
|
||||
'level',
|
||||
|
||||
# Additional fields used only for build order creation
|
||||
'create_child_builds',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
@ -88,6 +93,8 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
|
||||
'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)
|
||||
@ -112,6 +119,12 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
|
||||
|
||||
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.
|
||||
@ -136,13 +149,19 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
|
||||
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"""
|
||||
@ -151,6 +170,22 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
|
||||
|
||||
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".
|
||||
|
@ -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 = []
|
||||
|
@ -6,7 +6,7 @@ from django.urls import reverse
|
||||
|
||||
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 @@ class BuildTest(BuildAPITest):
|
||||
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.
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user