2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-15 19:45:46 +00:00

[Plugin] Auto-create builds (#9574)

* Remove existing "create child builds" functionality

- Remove API fields
- Remove background task definition

* Basic plugin structure

* Bump API version

* Bump API version

* Bug fix

* working on plugin event handling

* Add new stub method

* Implement functionality

* Fix conflicts in api_version

* Docs

* Fix docs

* Fix event type
This commit is contained in:
Oliver
2025-06-14 11:26:48 +10:00
committed by GitHub
parent 68c3a41f84
commit c027a7cf7d
9 changed files with 133 additions and 122 deletions

View File

@ -0,0 +1,21 @@
---
title: Auto Create Child Builds Plugin
---
## Auto Create Child Builds Plugin
The **Auto Create Child Builds Plugin** provides a mechanism to automatically create build orders for sub-assemblies when a higher level build order is issued.
### Activation
This plugin is an *optional* plugin, and must be enabled in the InvenTree settings.
Additionally, the {{ globalsetting("ENABLE_PLUGINS_EVENTS", short=True) }} setting must be enabled in the InvenTree plugin settings. This is required to allow plugins to respond to events in the InvenTree system.
## Usage
When this plugin is enabled, any time a build order is issued, the plugin will automatically create build orders for any sub-assemblies that are required by the issued build order.
This process is performed in the background, and does not require any user interaction.
Any new build orders that are created by this plugin will be marked as `PENDING`, and will require review and approval by the user before they can be issued.

View File

@ -10,7 +10,7 @@ The **Auto Issue Orders Plugin** provides a mechanism to automatically issue pen
This plugin is an *optional* plugin, and must be enabled in the InvenTree settings.
Additionally, the "Enable Schedule Integration" setting must be enabled in the InvenTree plugin settings. This is required to allow plugins to run scheduled tasks.
Additionally, the {{ globalsetting("ENABLE_PLUGINS_SCHEDULE", short=True) }} setting must be enabled in the InvenTree plugin settings. This is required to allow plugins to run scheduled tasks.
### Plugin Settings

View File

@ -14,6 +14,7 @@ The following builtin plugins are available in InvenTree:
| Plugin Name | Description | Mandatory |
| ----------- | ----------- | --------- |
| [Auto Create Child Builds](./auto_create_builds.md) | Automatically create child build orders for sub-assemblies | No |
| [Auto Issue Orders](./auto_issue.md) | Automatically issue pending orders when target date is reached | No |
| [BOM Exporter](./bom_exporter.md) | Custom [exporter](../mixins/export.md) for BOM data | Yes |
| [Currency Exchange](./currency_exchange.md) | Currency exchange rate plugin | Yes |

View File

@ -238,6 +238,7 @@ nav:
- Label Printer: plugins/machines/label_printer.md
- Builtin Plugins:
- Builtin Plugins: plugins/builtin/index.md
- Auto Create Builds: plugins/builtin/auto_create_builds.md
- Auto Issue: plugins/builtin/auto_issue.md
- BOM Exporter: plugins/builtin/bom_exporter.md
- Currency Exchange: plugins/builtin/currency_exchange.md

View File

@ -1,13 +1,16 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 348
INVENTREE_API_VERSION = 349
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v348 -> 2025-04-22 : https://github.com/inventree/InvenTree/pull/9312
v349 -> 2025-06-13 : https://github.com/inventree/InvenTree/pull/9574
- Remove the 'create_child_builds' flag from the BuildOrder creation API endpoint
v348 -> 2025-06-12 : https://github.com/inventree/InvenTree/pull/9312
- Adds "external" flag for BuildOrder
- Adds link between PurchaseOrderLineItem and BuildOrder

View File

@ -99,8 +99,6 @@ class BuildSerializer(
'responsible_detail',
'priority',
'level',
# Additional fields used only for build order creation
'create_child_builds',
]
read_only_fields = [
@ -149,14 +147,6 @@ class BuildSerializer(
source='project_code', many=False, read_only=True, allow_null=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.
@ -181,23 +171,16 @@ class BuildSerializer(
def __init__(self, *args, **kwargs):
"""Determine if extra serializer fields are required."""
part_detail = kwargs.pop('part_detail', True)
create = kwargs.pop('create', False)
kwargs.pop('create', False)
super().__init__(*args, **kwargs)
if isGeneratingSchema():
return
if not create:
self.fields.pop('create_child_builds', None)
if not part_detail:
self.fields.pop('part_detail', None)
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."""
# Ensure the reference matches the required pattern
@ -205,21 +188,6 @@ class BuildSerializer(
return reference
@transaction.atomic
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, group='build'
)
return build_order
class BuildOutputSerializer(serializers.Serializer):
"""Serializer for a "BuildOutput".
@ -1137,7 +1105,6 @@ class BuildAutoAllocationSerializer(serializers.Serializer):
def save(self):
"""Perform the auto-allocation step."""
import build.tasks
import InvenTree.tasks
data = self.validated_data

View File

@ -1,12 +1,9 @@
"""Background task definitions for the BuildOrder app."""
import random
import time
from datetime import timedelta
from decimal import Decimal
from django.contrib.auth.models import User
from django.db import transaction
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
@ -199,59 +196,6 @@ def check_build_stock(build: build_models.Build):
)
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_models.Build.DoesNotExist, ValueError):
return
assembly_items = build_order.part.get_bom_items().filter(sub_part__assembly=True)
# Random delay, to reduce likelihood of race conditions from multiple build orders being created simultaneously
time.sleep(random.random())
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)
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):
"""Notify appropriate users that a Build has just become 'overdue'."""
targets = []

View File

@ -516,35 +516,6 @@ class BuildTest(BuildAPITest):
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

@ -0,0 +1,103 @@
"""Plugin to automatically create build orders for assemblies."""
from decimal import Decimal
from django.utils.translation import gettext_lazy as _
import structlog
from build.events import BuildEvents
from build.models import Build
from build.status_codes import BuildStatus
from plugin import InvenTreePlugin
from plugin.mixins import EventMixin
logger = structlog.get_logger('inventree')
class AutoCreateBuildsPlugin(EventMixin, InvenTreePlugin):
"""Plugin which automatically creates build orders for assemblies.
This plugin can be used to automatically create new build orders based on certain events:
- When a build order is issued, automatically generate child build orders for sub-assemblies
Build orders are only created for parts which are have insufficient stock to fulfill the order.
"""
NAME = _('Auto Create Builds')
SLUG = 'autocreatebuilds'
AUTHOR = _('InvenTree contributors')
DESCRIPTION = _('Automatically create build orders for assemblies')
VERSION = '1.0.0'
def wants_process_event(self, event) -> bool:
"""Return whether given event should be processed or not."""
return event in [BuildEvents.ISSUED]
def process_event(self, event, *args, **kwargs):
"""Process the triggered event."""
build_id = kwargs.get('id')
if not build_id:
logger.error('No build ID provided for event', event=event)
return
try:
build = Build.objects.get(pk=build_id)
except (ValueError, Build.DoesNotExist):
logger.error('Invalid build ID provided', build_id=build_id)
return
if event == BuildEvents.ISSUED:
self.process_build(build)
def process_build(self, build: Build):
"""Process a build order when it is issued."""
# Iterate through all sub-assemblies for the build order
for bom_item in build.part.get_bom_items().filter(
consumable=False, sub_part__assembly=True
):
subassembly = bom_item.sub_part
with_variants = bom_item.allow_variants
# Quantity required for this particular build order
build_quantity = bom_item.get_required_quantity(build.quantity)
# Total existing stock for the sub-assembly
stock_quantity = subassembly.get_stock_count(include_variants=with_variants)
# How many of the sub-assembly are already allocated?
allocated_quantity = subassembly.allocation_count(
include_variants=with_variants
)
# Quantity of item already scheduled for production?
in_production_quantity = subassembly.quantity_being_built
# Determine if there is sufficient stock for the sub-assembly
required_quantity = (
Decimal(build_quantity)
+ Decimal(subassembly.minimum_stock)
+ Decimal(allocated_quantity)
- Decimal(stock_quantity)
- Decimal(in_production_quantity)
)
if required_quantity <= 0:
# No need to build this sub-assembly
continue
# Generate a new build order for the sub-assembly
Build.objects.create(
part=subassembly,
parent=build,
project_code=build.project_code,
quantity=required_quantity,
responsible=build.responsible,
sales_order=build.sales_order,
start_date=build.start_date,
status=BuildStatus.PENDING,
target_date=build.target_date,
)