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:
21
docs/docs/plugins/builtin/auto_create_builds.md
Normal file
21
docs/docs/plugins/builtin/auto_create_builds.md
Normal 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.
|
@ -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
|
||||
|
||||
|
@ -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 |
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 = []
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
)
|
Reference in New Issue
Block a user