feat: back-update min picked qty for a bundle
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 3191f15..7564e8f 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -5,7 +5,7 @@
 from collections import OrderedDict, defaultdict
 from itertools import groupby
 from operator import itemgetter
-from typing import Set
+from typing import Dict, List, Set
 
 import frappe
 from frappe import _
@@ -40,7 +40,6 @@
 				)
 
 	def before_submit(self):
-
 		update_sales_orders = set()
 		for item in self.locations:
 			# if the user has not entered any picked qty, set it to stock_qty, before submit
@@ -70,6 +69,7 @@
 				title=_("Quantity Mismatch"),
 			)
 
+		self.update_bundle_picked_qty()
 		self.update_sales_order_picking_status(update_sales_orders)
 
 	def before_cancel(self):
@@ -81,6 +81,7 @@
 				self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code)
 				updated_sales_orders.add(item.sales_order)
 
+		self.update_bundle_picked_qty()
 		self.update_sales_order_picking_status(updated_sales_orders)
 
 	def update_sales_order_item(self, item, picked_qty, item_code):
@@ -223,6 +224,56 @@
 		for idx, item in enumerate(self.locations, start=1):
 			item.idx = idx
 
+	def update_bundle_picked_qty(self):
+		"""Ensure that picked quantity is sufficient for fulfilling a whole number of."""
+
+		product_bundles = self._get_product_bundles()
+		product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values())
+
+		for so_row, item_code in product_bundles.items():
+			picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code])
+			item_table = "Sales Order Item"
+			already_picked = frappe.db.get_value(item_table, so_row, "picked_qty")
+			frappe.db.set_value(
+				item_table,
+				so_row,
+				"picked_qty",
+				already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)),
+			)
+
+	def _get_product_bundles(self) -> Dict[str, str]:
+		# Dict[so_item_row: item_code]
+		product_bundles = {}
+		for item in self.locations:
+			if not item.product_bundle_item:
+				continue
+			bundle_item_code = frappe.db.get_value(
+				"Sales Order Item", item.product_bundle_item, "item_code"
+			)
+			product_bundles[item.product_bundle_item] = bundle_item_code
+		return product_bundles
+
+	def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]:
+		# bundle_item_code: Dict[component, qty]
+		product_bundle_qty_map = {}
+		for bundle_item_code in bundles:
+			bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code})
+			product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
+		return product_bundle_qty_map
+
+	def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> float:
+		"""Compute how many full bundles can be created from picked items."""
+		possible_bundles = []
+		for item in self.locations:
+			if item.product_bundle_item != bundle_row:
+				continue
+
+			if qty_in_bundle := bundle_items.get(item.item_code):
+				possible_bundles.append(item.picked_qty / qty_in_bundle)
+			else:
+				possible_bundles.append(0)
+		return min(possible_bundles)
+
 
 def validate_item_locations(pick_list):
 	if not pick_list.locations: