diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 53e515b0f8e..50a3a0ebfb4 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -550,6 +550,15 @@ class PickList(TransactionBase): def set_item_locations(self, save: bool = False): self.validate_for_qty() items = self.aggregate_item_qty() + + # Serialize concurrent allocations per item on postgres. MariaDB's gap locks on the + # picked-items locking read below already make two simultaneous allocations take turns; + # postgres locking reads can't see the rows another in-flight allocation is inserting, so + # both could claim the same stock. Sorted so overlapping documents can't deadlock. + if frappe.db.db_type == "postgres" and hasattr(frappe.db, "transaction_advisory_lock"): + for item_code in sorted({d.item_code for d in items}): + frappe.db.transaction_advisory_lock(("pick-allocate", item_code)) + picked_items_details = self.get_picked_items_details(items) self.item_location_map = frappe._dict() diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index e37cb0a3532..3ca41f29bb3 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -29,6 +29,23 @@ from erpnext.tests.utils import ERPNextTestSuite class TestPickList(ERPNextTestSuite): + def test_pick_list_allocation_takes_advisory_gate(self): + if frappe.db.db_type != "postgres": + return + + item = make_item(properties={"is_stock_item": 1}).name + make_stock_entry(item=item, to_warehouse="_Test Warehouse - _TC", qty=5, basic_rate=100) + sales_order = make_sales_order(item_code=item, warehouse="_Test Warehouse - _TC", qty=2, rate=100) + + def held_advisory_locks(): + return frappe.db.sql( + "SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid()" + )[0][0] + + before = held_advisory_locks() + create_pick_list(sales_order.name) + self.assertGreater(held_advisory_locks(), before) + def test_pick_list_picks_warehouse_for_each_item(self): item_code = make_item().name try: