2
0
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:
Oliver
2024-08-21 16:31:48 +10:00
committed by GitHub
parent 7709d8df70
commit 8474b7bf4c
8 changed files with 170 additions and 7 deletions

View File

@ -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

View File

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

View File

@ -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".

View File

@ -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 = []

View File

@ -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.

View File

@ -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;