fix: update `Packed Qty` in DN on submit and cancel of `Packing Slip`
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py
index 415c9e8..d1c122d 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.py
@@ -4,159 +4,107 @@
 
 import frappe
 from frappe import _
-from frappe.model import no_value_fields
-from frappe.model.document import Document
-from frappe.utils import cint, flt
+from frappe.utils import cint
+
+from erpnext.controllers.status_updater import StatusUpdater
 
 
-class PackingSlip(Document):
-	def validate(self):
-		"""
-		* Validate existence of submitted Delivery Note
-		* Case nos do not overlap
-		* Check if packed qty doesn't exceed actual qty of delivery note
+class PackingSlip(StatusUpdater):
+	def __init__(self, *args, **kwargs) -> None:
+		super(PackingSlip, self).__init__(*args, **kwargs)
+		self.status_updater = [
+			{
+				"target_dt": "Delivery Note Item",
+				"join_field": "dn_detail",
+				"target_field": "packed_qty",
+				"target_parent_dt": "Delivery Note",
+				"target_ref_field": "qty",
+				"source_dt": "Packing Slip Item",
+				"source_field": "qty",
+			},
+			{
+				"target_dt": "Packed Item",
+				"join_field": "pi_detail",
+				"target_field": "packed_qty",
+				"target_parent_dt": "Delivery Note",
+				"target_ref_field": "qty",
+				"source_dt": "Packing Slip Item",
+				"source_field": "qty",
+			},
+		]
 
-		It is necessary to validate case nos before checking quantity
-		"""
-		self.validate_delivery_note()
-		self.validate_items_mandatory()
-		self.validate_case_nos()
-		self.validate_qty()
-
+	def validate(self) -> None:
 		from erpnext.utilities.transaction_base import validate_uom_is_integer
 
+		self.validate_delivery_note()
+		self.validate_case_nos()
+
 		validate_uom_is_integer(self, "stock_uom", "qty")
 		validate_uom_is_integer(self, "weight_uom", "net_weight")
 
 		self.set_missing_values()
 
+	def on_submit(self):
+		self.update_prevdoc_status()
+
+	def on_cancel(self):
+		self.update_prevdoc_status()
+
 	def validate_delivery_note(self):
-		"""
-		Validates if delivery note has status as draft
-		"""
+		"""Raises an exception if the `Delivery Note` status is not Draft"""
+
 		if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0:
-			frappe.throw(_("Delivery Note {0} must not be submitted").format(self.delivery_note))
-
-	def validate_items_mandatory(self):
-		rows = [d.item_code for d in self.get("items")]
-		if not rows:
-			frappe.msgprint(_("No Items to pack"), raise_exception=1)
-
-	def validate_case_nos(self):
-		"""
-		Validate if case nos overlap. If they do, recommend next case no.
-		"""
-		if not cint(self.from_case_no):
-			frappe.msgprint(_("Please specify a valid 'From Case No.'"), raise_exception=1)
-		elif not self.to_case_no:
-			self.to_case_no = self.from_case_no
-		elif cint(self.from_case_no) > cint(self.to_case_no):
-			frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), raise_exception=1)
-
-		res = frappe.db.sql(
-			"""SELECT name FROM `tabPacking Slip`
-			WHERE delivery_note = %(delivery_note)s AND docstatus = 1 AND
-			((from_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s)
-			OR (to_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s)
-			OR (%(from_case_no)s BETWEEN from_case_no AND to_case_no))
-			""",
-			{
-				"delivery_note": self.delivery_note,
-				"from_case_no": self.from_case_no,
-				"to_case_no": self.to_case_no,
-			},
-		)
-
-		if res:
 			frappe.throw(
-				_("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no())
+				_("A Packing Slip can only be created for Draft Delivery Note.").format(self.delivery_note)
 			)
 
-	def validate_qty(self):
-		"""Check packed qty across packing slips and delivery note"""
-		# Get Delivery Note Items, Item Quantity Dict and No. of Cases for this Packing slip
-		dn_details, ps_item_qty, no_of_cases = self.get_details_for_packing()
+	def validate_case_nos(self):
+		"""Validate if case nos overlap. If they do, recommend next case no."""
 
-		for item in dn_details:
-			new_packed_qty = (flt(ps_item_qty[item["item_code"]]) * no_of_cases) + flt(item["packed_qty"])
-			if new_packed_qty > flt(item["qty"]) and no_of_cases:
-				self.recommend_new_qty(item, ps_item_qty, no_of_cases)
+		if not self.to_case_no:
+			self.to_case_no = self.from_case_no
+		elif cint(self.from_case_no) > cint(self.to_case_no):
+			frappe.throw(_("'To Package No.' cannot be less than 'From Package No.'"))
+		else:
+			ps = frappe.qb.DocType("Packing Slip")
+			res = (
+				frappe.qb.from_(ps)
+				.select(
+					ps.name,
+				)
+				.where(
+					(ps.delivery_note == self.delivery_note)
+					& (ps.docstatus == 1)
+					& (
+						(ps.from_case_no.between(self.from_case_no, self.to_case_no))
+						| (ps.to_case_no.between(self.from_case_no, self.to_case_no))
+						| ((ps.from_case_no <= self.from_case_no) & (ps.to_case_no >= self.from_case_no))
+					)
+				)
+			).run()
+
+			if res:
+				frappe.throw(
+					_("""Package No(s) already in use. Try from Package No {0}""").format(
+						self.get_recommended_case_no()
+					)
+				)
 
 	def set_missing_values(self):
 		if not self.from_case_no:
 			self.from_case_no = self.get_recommended_case_no()
 
 		for item in self.items:
-			weight_per_unit, weight_uom = frappe.db.get_value(
-				"Item", item.item_code, ["weight_per_unit", "weight_uom"]
+			stock_uom, weight_per_unit, weight_uom = frappe.db.get_value(
+				"Item", item.item_code, ["stock_uom", "weight_per_unit", "weight_uom"]
 			)
 
+			item.stock_uom = stock_uom
 			if weight_per_unit and not item.net_weight:
 				item.net_weight = weight_per_unit
 			if weight_uom and not item.weight_uom:
 				item.weight_uom = weight_uom
 
-	def get_details_for_packing(self):
-		"""
-		Returns
-		* 'Delivery Note Items' query result as a list of dict
-		* Item Quantity dict of current packing slip doc
-		* No. of Cases of this packing slip
-		"""
-
-		rows = [d.item_code for d in self.get("items")]
-
-		# also pick custom fields from delivery note
-		custom_fields = ", ".join(
-			"dni.`{0}`".format(d.fieldname)
-			for d in frappe.get_meta("Delivery Note Item").get_custom_fields()
-			if d.fieldtype not in no_value_fields
-		)
-
-		if custom_fields:
-			custom_fields = ", " + custom_fields
-
-		condition = ""
-		if rows:
-			condition = " and item_code in (%s)" % (", ".join(["%s"] * len(rows)))
-
-		# gets item code, qty per item code, latest packed qty per item code and stock uom
-		res = frappe.db.sql(
-			"""select item_code, sum(qty) as qty,
-			(select sum(psi.qty * (abs(ps.to_case_no - ps.from_case_no) + 1))
-				from `tabPacking Slip` ps, `tabPacking Slip Item` psi
-				where ps.name = psi.parent and ps.docstatus = 1
-				and ps.delivery_note = dni.parent and psi.item_code=dni.item_code) as packed_qty,
-			stock_uom, item_name, description, dni.batch_no {custom_fields}
-			from `tabDelivery Note Item` dni
-			where parent=%s {condition}
-			group by item_code""".format(
-				condition=condition, custom_fields=custom_fields
-			),
-			tuple([self.delivery_note] + rows),
-			as_dict=1,
-		)
-
-		ps_item_qty = dict([[d.item_code, d.qty] for d in self.get("items")])
-		no_of_cases = cint(self.to_case_no) - cint(self.from_case_no) + 1
-
-		return res, ps_item_qty, no_of_cases
-
-	def recommend_new_qty(self, item, ps_item_qty, no_of_cases):
-		"""
-		Recommend a new quantity and raise a validation exception
-		"""
-		item["recommended_qty"] = (flt(item["qty"]) - flt(item["packed_qty"])) / no_of_cases
-		item["specified_qty"] = flt(ps_item_qty[item["item_code"]])
-		if not item["packed_qty"]:
-			item["packed_qty"] = 0
-
-		frappe.throw(
-			_("Quantity for Item {0} must be less than {1}").format(
-				item.get("item_code"), item.get("recommended_qty")
-			)
-		)
-
 	def get_recommended_case_no(self):
 		"""Returns the next case no. for a new packing slip for a delivery note"""