diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 0c36814815..6e1fd58939 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -1186,7 +1186,8 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models. BuildLine.objects.bulk_create(lines) - logger.info(f"Created {len(lines)} BuildLine objects for BuildOrder") + if len(lines) > 0: + logger.info(f"Created {len(lines)} BuildLine objects for BuildOrder") @transaction.atomic def update_build_line_items(self): diff --git a/InvenTree/build/tasks.py b/InvenTree/build/tasks.py index 6d63720e96..95e7baa474 100644 --- a/InvenTree/build/tasks.py +++ b/InvenTree/build/tasks.py @@ -24,6 +24,55 @@ import part.models as part_models logger = logging.getLogger('inventree') +def update_build_order_lines(bom_item_pk: int): + """Update all BuildOrderLineItem objects which reference a particular BomItem. + + This task is triggered when a BomItem is created or updated. + """ + + logger.info(f"Updating build order lines for BomItem {bom_item_pk}") + + bom_item = part_models.BomItem.objects.filter(pk=bom_item_pk).first() + + # If the BomItem has been deleted, there is nothing to do + if not bom_item: + return + + assemblies = bom_item.get_assemblies() + + # Find all active builds which reference any of the parts + builds = build.models.Build.objects.filter( + part__in=list(assemblies), + status__in=BuildStatusGroups.ACTIVE_CODES + ) + + # Iterate through each build, and update the relevant line items + for bo in builds: + # Try to find a matching build order line + line = build.models.BuildLine.objects.filter( + build=bo, + bom_item=bom_item, + ).first() + + q = bom_item.get_required_quantity(bo.quantity) + + if line: + # Ensure quantity is correct + if line.quantity != q: + line.quantity = q + line.save() + else: + # Create a new line item + build.models.BuildLine.objects.create( + build=bo, + bom_item=bom_item, + quantity=q, + ) + + if builds.count() > 0: + logger.info(f"Updated {builds.count()} build orders for part {bom_item.part}") + + def check_build_stock(build: build.models.Build): """Check the required stock for a newly created build order. diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 1133189f28..e0fab78d62 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -3749,6 +3749,18 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model): """Return the list API endpoint URL associated with the BomItem model""" return reverse('api-bom-list') + def get_assemblies(self): + """Return a list of assemblies which use this BomItem""" + + assemblies = [self.part] + + if self.inherited: + assemblies += list( + self.part.get_descendants(include_self=False) + ) + + return assemblies + def get_valid_parts_for_allocation(self, allow_variants=True, allow_substitutes=True): """Return a list of valid parts which can be allocated against this BomItem. @@ -4048,6 +4060,18 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model): return "{pmin} to {pmax}".format(pmin=pmin, pmax=pmax) +@receiver(post_save, sender=BomItem, dispatch_uid='update_bom_build_lines') +def update_bom_build_lines(sender, instance, created, **kwargs): + """Update existing build orders when a BomItem is created or edited""" + + if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData(): + import build.tasks + InvenTree.tasks.offload_task( + build.tasks.update_build_order_lines, + instance.pk + ) + + @receiver(post_save, sender=BomItem, dispatch_uid='post_save_bom_item') @receiver(post_save, sender=PartSellPriceBreak, dispatch_uid='post_save_sale_price_break') @receiver(post_save, sender=PartInternalPriceBreak, dispatch_uid='post_save_internal_price_break')