diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index 85053dabf4..236641b69c 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -180,6 +180,7 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre return reference + @transaction.atomic def create(self, validated_data): """Save the Build object.""" @@ -192,7 +193,7 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre InvenTree.tasks.offload_task( build.tasks.create_child_builds, build_order.pk, - group='build', + group='build' ) return build_order diff --git a/src/backend/InvenTree/build/tasks.py b/src/backend/InvenTree/build/tasks.py index fbdf4f39c3..0df3233179 100644 --- a/src/backend/InvenTree/build/tasks.py +++ b/src/backend/InvenTree/build/tasks.py @@ -1,11 +1,15 @@ """Background task definitions for the BuildOrder app.""" import logging +import random +import time + from datetime import timedelta from decimal import Decimal from django.contrib.auth.models import User from django.template.loader import render_to_string +from django.db import transaction from django.utils.translation import gettext_lazy as _ from allauth.account.models import EmailAddress @@ -198,27 +202,49 @@ def create_child_builds(build_id: int) -> None: assembly_items = build_order.part.get_bom_items().filter(sub_part__assembly=True) - for item in assembly_items: - quantity = item.quantity * build_order.quantity + # Random delay, to reduce likelihood of race conditions from multiple build orders being created simultaneously + time.sleep(random.random()) - 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, - ) + with transaction.atomic(): + # Atomic transaction to ensure that all child build orders are created together, or not at all + # This is critical to prevent duplicate child build orders being created (e.g. if the task is re-run) - # Offload the child build order creation to the background task queue - InvenTree.tasks.offload_task( - create_child_builds, - sub_order.pk, - group='build' - ) + sub_build_ids = [] + + for item in assembly_items: + quantity = item.quantity * build_order.quantity + + + # Check if the child build order has already been created + if build_models.Build.objects.filter( + part=item.sub_part, + parent=build_order, + quantity=quantity, + status__in=BuildStatusGroups.ACTIVE_CODES + ).exists(): + continue + + 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, + ) + + sub_build_ids.append(sub_order.pk) + + for pk in sub_build_ids: + # Offload the child build order creation to the background task queue + InvenTree.tasks.offload_task( + create_child_builds, + pk, + group='build' + ) def notify_overdue_build_order(bo: build_models.Build):