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

1"""Plugin to automatically create build orders for assemblies.""" 

2 

3from decimal import Decimal 

4 

5from django.utils.translation import gettext_lazy as _ 

6 

7import structlog 

8 

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 

14 

15logger = structlog.get_logger('inventree') 

16 

17 

18class AutoCreateBuildsPlugin(EventMixin, InvenTreePlugin): 

19 """Plugin which automatically creates build orders for assemblies. 

20 

21 This plugin can be used to automatically create new build orders based on certain events: 

22 

23 - When a build order is issued, automatically generate child build orders for sub-assemblies 

24 

25 Build orders are only created for parts which are have insufficient stock to fulfill the order. 

26 """ 

27 

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' 

33 

34 def wants_process_event(self, event) -> bool: 

35 """Return whether given event should be processed or not.""" 

36 return event in [BuildEvents.ISSUED] 

37 

38 def process_event(self, event, *args, **kwargs): 

39 """Process the triggered event.""" 

40 build_id = kwargs.get('id') 

41 

42 if not build_id: 

43 logger.error('No build ID provided for event', event=event) 

44 return 

45 

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 

51 

52 if event == BuildEvents.ISSUED: 

53 self.process_build(build) 

54 

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 

62 

63 with_variants = bom_item.allow_variants 

64 

65 # Quantity required for this particular build order 

66 build_quantity = bom_item.get_required_quantity(build.quantity) 

67 

68 # Total existing stock for the sub-assembly 

69 stock_quantity = subassembly.get_stock_count(include_variants=with_variants) 

70 

71 # How many of the sub-assembly are already allocated? 

72 allocated_quantity = subassembly.allocation_count( 

73 include_variants=with_variants 

74 ) 

75 

76 # Quantity of item already scheduled for production? 

77 in_production_quantity = subassembly.quantity_being_built 

78 

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 ) 

87 

88 if required_quantity <= 0: 

89 # No need to build this sub-assembly 

90 continue 

91 

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 )