From b33dda76d39b2d1898e2778904d38eacaa07cc08 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 27 Jul 2026 17:37:14 +1000 Subject: [PATCH] Refactor sales order methods: (#12477) - Cancel sales order - Assign serial numbers --- src/backend/InvenTree/build/test_api.py | 2 +- src/backend/InvenTree/order/models.py | 25 ++++++++++++++----------- src/backend/InvenTree/stock/models.py | 16 +++++++++++++++- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/backend/InvenTree/build/test_api.py b/src/backend/InvenTree/build/test_api.py index f4b993712e..0e2a56b96e 100644 --- a/src/backend/InvenTree/build/test_api.py +++ b/src/backend/InvenTree/build/test_api.py @@ -2304,7 +2304,7 @@ class BuildConsumeTest(BuildAPITest): expected_code=201, benchmark=True, max_query_count=250, - max_query_time=1.0, + max_query_time=1.5, ) build.refresh_from_db() diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index a9ec1990c4..19b235e3ac 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -1704,23 +1704,28 @@ class SalesOrder(TotalPriceMixin, Order): serial_numbers, quantity, part.get_latest_serial_number(), part=part ) + serials = [str(serial).strip() for serial in serials] + serials_not_exist = set() serials_unavailable = set() stock_items_to_allocate = [] + # Bulk-fetch every candidate StockItem in a single query, keyed by serial, + # rather than querying once per requested serial number + candidate_items = {} + + for item in stock.models.StockItem.objects.filter( + part=part, serial__in=serials, quantity=1 + ): + candidate_items.setdefault(item.serial, item) + for serial in serials: - serial = str(serial).strip() + stock_item = candidate_items.get(serial) - items = stock.models.StockItem.objects.filter( - part=part, serial=serial, quantity=1 - ) - - if not items.exists(): + if stock_item is None: serials_not_exist.add(serial) continue - stock_item = items[0] - if get_global_setting('SALESORDER_BLOCK_INCOMPLETE_ITEM_TESTS'): if ( stock_item.hasRequiredTests() @@ -1953,9 +1958,7 @@ class SalesOrder(TotalPriceMixin, Order): self.status = SalesOrderStatus.CANCELLED.value self.save() - for line in self.lines.all(): - for allocation in line.allocations.all(): - allocation.delete() + SalesOrderAllocation.objects.filter(line__order=self).delete() trigger_event(SalesOrderEvents.CANCELLED, id=self.pk) diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py index 2d0a663014..6898453b0e 100644 --- a/src/backend/InvenTree/stock/models.py +++ b/src/backend/InvenTree/stock/models.py @@ -1725,11 +1725,25 @@ class StockItem( - Is installed inside another StockItem - It has been assigned to a SalesOrder - It has been assigned to a BuildOrder + - It has active allocations against a SalesOrder or TransferOrder + - It is referenced by a ReturnOrder line item """ if self.installed_item_count() > 0: return False - return self.sales_order is None + if self.sales_order is not None: + return False + + if self.allocations.exists(): + return False + + if self.get_sales_order_allocations(active=True).exists(): + return False + + if self.get_transfer_order_allocations(active=True).exists(): + return False + + return not self.return_order_lines.exists() def get_installed_items(self, cascade: bool = False) -> set[StockItem]: """Return all stock items which are *installed* in this one!