From 6066dabe43d09373c3404bd9f151961cb456b0cd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 23 May 2026 07:44:17 +0000 Subject: [PATCH] Offload order completion tasks --- src/backend/InvenTree/order/models.py | 65 +++++++++++---------------- src/backend/InvenTree/order/tasks.py | 55 ++++++++++++++++++++++- 2 files changed, 81 insertions(+), 39 deletions(-) diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 5973f4292b..d0d851c215 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -849,24 +849,17 @@ class PurchaseOrder(TotalPriceMixin, Order): Order must be currently PLACED. """ - if self.status == PurchaseOrderStatus.PLACED: - self.status = PurchaseOrderStatus.COMPLETE.value - self.complete_date = InvenTree.helpers.current_date() + if self.status != PurchaseOrderStatus.PLACED: + return - self.save() + # Schedule the order completion task, which will perform the necessary stock movements and other related actions + InvenTree.tasks.offload_task( + order.tasks.complete_purchase_order, self.pk, group='purchase_order' + ) - unique_parts = set() - - # Schedule pricing update for any referenced parts - for line in self.lines.all().prefetch_related('part__part'): - # Ensure we only check 'unique' parts - if line.part and line.part.part: - unique_parts.add(line.part.part) - - for part in unique_parts: - part.schedule_pricing_update(create=True, refresh=False) - - trigger_event(PurchaseOrderEvents.COMPLETED, id=self.pk) + self.status = PurchaseOrderStatus.COMPLETE.value + self.complete_date = InvenTree.helpers.current_date() + self.save() @transaction.atomic def issue_order(self): @@ -1584,20 +1577,15 @@ class SalesOrder(TotalPriceMixin, Order): if not self.can_complete(**kwargs): return False + # Offload task to process the shipment lines + InvenTree.tasks.offload_task( + order.tasks.complete_sales_order, self.pk, group='sales_order' + ) + bypass_shipped = InvenTree.helpers.str2bool( get_global_setting('SALESORDER_SHIP_COMPLETE') ) - # Update line items - for line in self.lines.all(): - # Mark any "virtual" parts as shipped at this point - if line.part and line.part.virtual and line.shipped != line.quantity: - line.shipped = line.quantity - line.save() - - if line.part: - line.part.schedule_pricing_update(create=True) - if bypass_shipped or self.status == SalesOrderStatus.SHIPPED: self.status = SalesOrderStatus.COMPLETE.value else: @@ -1608,9 +1596,6 @@ class SalesOrder(TotalPriceMixin, Order): self.shipment_date = InvenTree.helpers.current_date() self.save() - - trigger_event(SalesOrderEvents.COMPLETED, id=self.pk) - return True @property @@ -3459,19 +3444,23 @@ class TransferOrder(Order): if not self.can_complete(raise_error=True, **kwargs): return False - if self.status == TransferOrderStatus.ISSUED: - for allocation in self.allocations(): - # execute each transfer - allocation.complete_allocation(user) + if self.status != TransferOrderStatus.ISSUED: + return - self.status = TransferOrderStatus.COMPLETE.value - self.complete_date = InvenTree.helpers.current_date() + # Offload task to perform the transfer of each allocated stock item + InvenTree.tasks.offload_task( + order.tasks.complete_transfer_order, + self.pk, + user.pk if user else None, + group='transfer_order', + ) - self.save() + self.status = TransferOrderStatus.COMPLETE.value + self.complete_date = InvenTree.helpers.current_date() - trigger_event(TransferOrderEvents.COMPLETED, id=self.pk) + self.save() - return True + return True @transaction.atomic def complete_order(self, user, **kwargs): diff --git a/src/backend/InvenTree/order/tasks.py b/src/backend/InvenTree/order/tasks.py index d9065c2d20..84bc0d187d 100644 --- a/src/backend/InvenTree/order/tasks.py +++ b/src/backend/InvenTree/order/tasks.py @@ -15,7 +15,7 @@ import common.notifications import InvenTree.helpers_model import order.models from InvenTree.tasks import ScheduledTask, scheduled_task -from order.events import PurchaseOrderEvents, SalesOrderEvents +from order.events import PurchaseOrderEvents, SalesOrderEvents, TransferOrderEvents from order.status_codes import ( PurchaseOrderStatusGroups, ReturnOrderStatusGroups, @@ -273,3 +273,56 @@ def complete_sales_order_shipment( # Trigger event signalling that the shipment has been completed trigger_event(SalesOrderEvents.SHIPMENT_COMPLETE, id=shipment.pk) + + +@tracer.start_as_current_span('complete_purchase_order') +def complete_purchase_order(order_id: int): + """Run completion tasks for a PurchaseOrder that has just been marked as complete.""" + po = order.models.PurchaseOrder.objects.get(pk=order_id) + + unique_parts = set() + + # Schedule pricing update for any referenced parts + for line in po.lines.all().prefetch_related('part__part'): + # Ensure we only check 'unique' parts + if line.part and line.part.part: + unique_parts.add(line.part.part) + + for part in unique_parts: + part.schedule_pricing_update(create=True, refresh=False) + + trigger_event(PurchaseOrderEvents.COMPLETED, id=order_id) + + # Trigger event signalling that the purchase order has been completed + trigger_event(PurchaseOrderEvents.COMPLETE, id=order_id) + + +@tracer.start_as_current_span('complete_sales_order') +def complete_sales_order(order_id: int): + """Run completion tasks for a SalesOrder that has just been marked as complete.""" + so = order.models.SalesOrder.objects.get(pk=order_id) + + # Update line items + for line in so.lines.all(): + # Mark any "virtual" parts as shipped at this point + if line.part and line.part.virtual and line.shipped != line.quantity: + line.shipped = line.quantity + line.save() + + if line.part: + line.part.schedule_pricing_update(create=True) + + trigger_event(SalesOrderEvents.COMPLETED, id=order_id) + + +@tracer.start_as_current_span('complete_transfer_order') +def complete_transfer_order(order_id: int, user_id: int): + """Run completion tasks for a TransferOrder that has just been marked as complete.""" + transfer_order = order.models.TransferOrder.objects.get(pk=order_id) + user = User.objects.filter(pk=user_id).first() if user_id else None + + for allocation in transfer_order.allocations(): + # execute each transfer + allocation.complete_allocation(user) + + trigger_event(TransferOrderEvents.COMPLETED, id=order_id)