Coverage for src/backend/InvenTree/plugin/builtin/events/auto_create_builds.py: 37%
41 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Plugin to automatically create build orders for assemblies."""
3from decimal import Decimal
5from django.utils.translation import gettext_lazy as _
7import structlog
9from build.events import BuildEvents
10from build.models import Build
11from build.status_codes import BuildStatus
12from plugin import InvenTreePlugin
13from plugin.mixins import EventMixin
15logger = structlog.get_logger('inventree')
18class AutoCreateBuildsPlugin(EventMixin, InvenTreePlugin):
19 """Plugin which automatically creates build orders for assemblies.
21 This plugin can be used to automatically create new build orders based on certain events:
23 - When a build order is issued, automatically generate child build orders for sub-assemblies
25 Build orders are only created for parts which are have insufficient stock to fulfill the order.
26 """
28 NAME = _('Auto Create Builds')
29 SLUG = 'autocreatebuilds'
30 AUTHOR = _('InvenTree contributors')
31 DESCRIPTION = _('Automatically create build orders for assemblies')
32 VERSION = '1.1.0'
34 def wants_process_event(self, event) -> bool:
35 """Return whether given event should be processed or not."""
36 return event in [BuildEvents.ISSUED]
38 def process_event(self, event, *args, **kwargs):
39 """Process the triggered event."""
40 build_id = kwargs.get('id')
42 if not build_id:
43 logger.error('No build ID provided for event', event=event)
44 return
46 try:
47 build = Build.objects.get(pk=build_id)
48 except (ValueError, Build.DoesNotExist):
49 logger.error('Invalid build ID provided', build_id=build_id)
50 return
52 if event == BuildEvents.ISSUED:
53 self.process_build(build)
55 def process_build(self, build: Build):
56 """Process a build order when it is issued."""
57 # Iterate through all sub-assemblies for the build order
58 for bom_item in build.part.get_bom_items().filter(
59 consumable=False, sub_part__assembly=True
60 ):
61 subassembly = bom_item.sub_part
63 with_variants = bom_item.allow_variants
65 # Quantity required for this particular build order
66 build_quantity = bom_item.get_required_quantity(build.quantity)
68 # Total existing stock for the sub-assembly
69 stock_quantity = subassembly.get_stock_count(include_variants=with_variants)
71 # How many of the sub-assembly are already allocated?
72 allocated_quantity = subassembly.allocation_count(
73 include_variants=with_variants
74 )
76 # Quantity of item already scheduled for production?
77 in_production_quantity = subassembly.quantity_being_built
79 # Determine if there is sufficient stock for the sub-assembly
80 required_quantity = (
81 Decimal(build_quantity)
82 + Decimal(subassembly.minimum_stock)
83 + Decimal(allocated_quantity)
84 - Decimal(stock_quantity)
85 - Decimal(in_production_quantity)
86 )
88 if required_quantity <= 0:
89 # No need to build this sub-assembly
90 continue
92 # Generate a new build order for the sub-assembly
93 Build.objects.create(
94 part=subassembly,
95 parent=build,
96 title=f'Generated child build for {build.reference}',
97 project_code=build.project_code,
98 quantity=required_quantity,
99 responsible=build.responsible,
100 sales_order=build.sales_order,
101 start_date=build.start_date,
102 status=BuildStatus.PENDING,
103 target_date=build.target_date,
104 )