perf: single update per Sales Order.

For each SO item the sales order picking status was being updated, this
isn't required and wasteful.
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 1d172ad..a35e16f 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -385,6 +385,16 @@
 		if tot_qty != 0:
 			self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False)
 
+	def update_picking_status(self):
+		total_picked_qty = 0.0
+		total_qty = 0.0
+		for so_item in self.items:
+			total_picked_qty += flt(so_item.picked_qty)
+			total_qty += flt(so_item.stock_qty)
+		per_picked = total_picked_qty / total_qty * 100
+
+		self.db_set("per_picked", flt(per_picked), update_modified=False)
+
 	def set_indicator(self):
 		"""Set indicator for portal"""
 		if self.per_billed < 100 and self.per_delivered < 100:
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 01448be..3191f15 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -5,6 +5,7 @@
 from collections import OrderedDict, defaultdict
 from itertools import groupby
 from operator import itemgetter
+from typing import Set
 
 import frappe
 from frappe import _
@@ -39,6 +40,8 @@
 				)
 
 	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
 			if item.picked_qty == 0:
@@ -47,6 +50,7 @@
 			if item.sales_order_item:
 				# update the picked_qty in SO Item
 				self.update_sales_order_item(item, item.picked_qty, item.item_code)
+				update_sales_orders.add(item.sales_order)
 
 			if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
 				continue
@@ -66,11 +70,18 @@
 				title=_("Quantity Mismatch"),
 			)
 
+		self.update_sales_order_picking_status(update_sales_orders)
+
 	def before_cancel(self):
-		# update picked_qty in SO Item on cancel of PL
+		"""Deduct picked qty on cancelling pick list"""
+		updated_sales_orders = set()
+
 		for item in self.get("locations"):
 			if item.sales_order_item:
 				self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code)
+				updated_sales_orders.add(item.sales_order)
+
+		self.update_sales_order_picking_status(updated_sales_orders)
 
 	def update_sales_order_item(self, item, picked_qty, item_code):
 		item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item"
@@ -91,17 +102,11 @@
 
 		frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty)
 
-		# TODO: only do this once after all items
-		sales_order = frappe.get_doc("Sales Order", item.sales_order)
-		total_picked_qty = 0
-		total_so_qty = 0
-		for so_item in sales_order.get("items"):
-			total_picked_qty += flt(so_item.picked_qty)
-			total_so_qty += flt(so_item.stock_qty)
-		total_picked_qty = total_picked_qty + picked_qty
-		per_picked = total_picked_qty / total_so_qty * 100
-
-		sales_order.db_set("per_picked", flt(per_picked), update_modified=False)
+	@staticmethod
+	def update_sales_order_picking_status(sales_orders: Set[str]) -> None:
+		for sales_order in sales_orders:
+			if sales_order:
+				frappe.get_doc("Sales Order", sales_order).update_picking_status()
 
 	@frappe.whitelist()
 	def set_item_locations(self, save=False):