Merge pull request #37954 from ruthra-kumar/expense_claim_repost

refactor: expand repost to `Expense Claim` and make it configurable
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 0203c45..b3ae627 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -154,7 +154,7 @@
 		frm.events.set_dynamic_labels(frm);
 		frm.events.show_general_ledger(frm);
 		erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
-		if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) {
+		if((frm.doc.references) && (frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0}))) {
 			frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() {
 				frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name});
 			}, __('Actions'));
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index d2adc51..171cc0c 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1783,9 +1783,14 @@
 		set_advance_flag(company="_Test Company", flag=0, default_account="")
 
 	def test_gl_entries_for_standalone_debit_note(self):
-		make_purchase_invoice(qty=5, rate=500, update_stock=True)
+		from erpnext.stock.doctype.item.test_item import make_item
 
-		returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True)
+		item_code = make_item(properties={"is_stock_item": 1})
+		make_purchase_invoice(item_code=item_code, qty=5, rate=500, update_stock=True)
+
+		returned_inv = make_purchase_invoice(
+			item_code=item_code, qty=-5, rate=5, update_stock=True, is_return=True
+		)
 
 		# override the rate with valuation rate
 		sle = frappe.get_all(
@@ -1795,7 +1800,7 @@
 		)[0]
 
 		rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
-		self.assertAlmostEqual(returned_inv.items[0].rate, rate)
+		self.assertAlmostEqual(rate, 500)
 
 	def test_payment_allocation_for_payment_terms(self):
 		from erpnext.buying.doctype.purchase_order.test_purchase_order import (
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index e5adeae..cd725b9 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -26,6 +26,7 @@
   "is_return",
   "return_against",
   "update_billed_amount_in_sales_order",
+  "update_billed_amount_in_delivery_note",
   "is_debit_note",
   "amended_from",
   "accounting_dimensions_section",
@@ -2153,6 +2154,13 @@
    "fieldname": "use_company_roundoff_cost_center",
    "fieldtype": "Check",
    "label": "Use Company default Cost Center for Round off"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: doc.is_return",
+   "fieldname": "update_billed_amount_in_delivery_note",
+   "fieldtype": "Check",
+   "label": "Update Billed Amount in Delivery Note"
   }
  ],
  "icon": "fa fa-file-text",
@@ -2165,7 +2173,7 @@
    "link_fieldname": "consolidated_invoice"
   }
  ],
- "modified": "2023-07-25 16:02:18.988799",
+ "modified": "2023-11-03 14:39:38.012346",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index f6d9c93..fa95ccd 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -253,6 +253,7 @@
 
 		self.update_status_updater_args()
 		self.update_prevdoc_status()
+
 		self.update_billing_status_in_dn()
 		self.clear_unallocated_mode_of_payments()
 
@@ -1019,7 +1020,7 @@
 
 	def make_customer_gl_entry(self, gl_entries):
 		# Checked both rounding_adjustment and rounded_total
-		# because rounded_total had value even before introcution of posting GLE based on rounded total
+		# because rounded_total had value even before introduction of posting GLE based on rounded total
 		grand_total = (
 			self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
 		)
@@ -1267,7 +1268,7 @@
 				if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
 					payment_mode.base_amount -= flt(self.change_amount)
 
-				if payment_mode.amount:
+				if payment_mode.base_amount:
 					# POS, make payment entries
 					gl_entries.append(
 						self.get_gl_dict(
@@ -1429,6 +1430,8 @@
 			)
 
 	def update_billing_status_in_dn(self, update_modified=True):
+		if self.is_return and not self.update_billed_amount_in_delivery_note:
+			return
 		updated_delivery_notes = []
 		for d in self.get("items"):
 			if d.dn_detail:
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index f24a24e..12e4003 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -117,7 +117,7 @@
 		for ple in self.ple_entries:
 			# get the balance object for voucher_type
 
-			if self.filters.get("ingore_accounts"):
+			if self.filters.get("ignore_accounts"):
 				key = (ple.voucher_type, ple.voucher_no, ple.party)
 			else:
 				key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
@@ -188,7 +188,7 @@
 			):
 				return
 
-		if self.filters.get("ingore_accounts"):
+		if self.filters.get("ignore_accounts"):
 			key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
 		else:
 			key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
@@ -200,7 +200,7 @@
 			if ple.against_voucher_no in self.return_entries:
 				return_against = self.return_entries.get(ple.against_voucher_no)
 				if return_against:
-					if self.filters.get("ingore_accounts"):
+					if self.filters.get("ignore_accounts"):
 						key = (ple.against_voucher_type, return_against, ple.party)
 					else:
 						key = (ple.account, ple.against_voucher_type, return_against, ple.party)
@@ -209,7 +209,7 @@
 
 		if not row:
 			# no invoice, this is an invoice / stand-alone payment / credit note
-			if self.filters.get("ingore_accounts"):
+			if self.filters.get("ignore_accounts"):
 				row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
 			else:
 				row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 1c7052f..e0adac4 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -10,7 +10,7 @@
 from frappe import _, qb, throw
 from frappe.model.meta import get_field_precision
 from frappe.query_builder import AliasedQuery, Criterion, Table
-from frappe.query_builder.functions import Sum
+from frappe.query_builder.functions import Round, Sum
 from frappe.query_builder.utils import DocType
 from frappe.utils import (
 	cint,
@@ -536,6 +536,8 @@
 		)
 
 	else:
+		precision = frappe.get_precision("Payment Entry", "unallocated_amount")
+
 		payment_entry = frappe.qb.DocType("Payment Entry")
 		payment_ref = frappe.qb.DocType("Payment Entry Reference")
 
@@ -557,7 +559,10 @@
 				.where(payment_ref.allocated_amount == args.get("unreconciled_amount"))
 			)
 		else:
-			q = q.where(payment_entry.unallocated_amount == args.get("unreconciled_amount"))
+			q = q.where(
+				Round(payment_entry.unallocated_amount, precision)
+				== Round(args.get("unreconciled_amount"), precision)
+			)
 
 	ret = q.run(as_dict=True)
 
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index e2a4b29..84a428c 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -780,6 +780,15 @@
 def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None):
 	asset_doc = frappe.get_doc("Asset", asset)
 
+	if asset_doc.available_for_use_date > getdate(disposal_date):
+		frappe.throw(
+			"Disposal date {0} cannot be before available for use date {1} of the asset.".format(
+				disposal_date, asset_doc.available_for_use_date
+			)
+		)
+	elif asset_doc.available_for_use_date == getdate(disposal_date):
+		return flt(asset_doc.gross_purchase_amount - asset_doc.opening_accumulated_depreciation)
+
 	if not asset_doc.calculate_depreciation:
 		return flt(asset_doc.value_after_depreciation)
 
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index b1da97d..2b6ffb7 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -470,6 +470,7 @@
    "fieldname": "material_request",
    "fieldtype": "Link",
    "label": "Material Request",
+   "mandatory_depends_on": "eval: doc.material_request_item",
    "no_copy": 1,
    "oldfieldname": "prevdoc_docname",
    "oldfieldtype": "Link",
@@ -485,6 +486,7 @@
    "fieldtype": "Data",
    "hidden": 1,
    "label": "Material Request Item",
+   "mandatory_depends_on": "eval: doc.material_request",
    "no_copy": 1,
    "oldfieldname": "prevdoc_detail_docname",
    "oldfieldtype": "Data",
@@ -914,7 +916,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-10-27 15:50:42.655573",
+ "modified": "2023-11-06 11:00:53.596417",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Purchase Order Item",
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 3a802bd..ece08d8 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -105,26 +105,26 @@
 	def set_rate_for_standalone_debit_note(self):
 		if self.get("is_return") and self.get("update_stock") and not self.return_against:
 			for row in self.items:
+				if row.rate <= 0:
+					# override the rate with valuation rate
+					row.rate = get_incoming_rate(
+						{
+							"item_code": row.item_code,
+							"warehouse": row.warehouse,
+							"posting_date": self.get("posting_date"),
+							"posting_time": self.get("posting_time"),
+							"qty": row.qty,
+							"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
+							"company": self.company,
+							"voucher_type": self.doctype,
+							"voucher_no": self.name,
+						},
+						raise_error_if_no_rate=False,
+					)
 
-				# override the rate with valuation rate
-				row.rate = get_incoming_rate(
-					{
-						"item_code": row.item_code,
-						"warehouse": row.warehouse,
-						"posting_date": self.get("posting_date"),
-						"posting_time": self.get("posting_time"),
-						"qty": row.qty,
-						"serial_and_batch_bundle": row.get("serial_and_batch_bundle"),
-						"company": self.company,
-						"voucher_type": self.doctype,
-						"voucher_no": self.name,
-					},
-					raise_error_if_no_rate=False,
-				)
-
-				row.discount_percentage = 0.0
-				row.discount_amount = 0.0
-				row.margin_rate_or_amount = 0.0
+					row.discount_percentage = 0.0
+					row.discount_amount = 0.0
+					row.margin_rate_or_amount = 0.0
 
 	def set_missing_values(self, for_validate=False):
 		super(BuyingController, self).set_missing_values(for_validate)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 4a00416..49386c4 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -36,6 +36,7 @@
   "prod_plan_references",
   "section_break_24",
   "combine_sub_items",
+  "sub_assembly_warehouse",
   "section_break_ucc4",
   "skip_available_sub_assembly_item",
   "column_break_igxl",
@@ -416,13 +417,19 @@
   {
    "fieldname": "column_break_igxl",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "sub_assembly_warehouse",
+   "fieldtype": "Link",
+   "label": "Sub Assembly Warehouse",
+   "options": "Warehouse"
   }
  ],
  "icon": "fa fa-calendar",
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2023-09-29 11:41:03.246059",
+ "modified": "2023-11-03 14:08:11.928027",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Production Plan",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 1850d1e..6b12a29 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -490,6 +490,12 @@
 				bin = frappe.get_doc("Bin", bin_name, for_update=True)
 				bin.update_reserved_qty_for_production_plan()
 
+		for d in self.sub_assembly_items:
+			if d.fg_warehouse and d.type_of_manufacturing == "In House":
+				bin_name = get_or_make_bin(d.production_item, d.fg_warehouse)
+				bin = frappe.get_doc("Bin", bin_name, for_update=True)
+				bin.update_reserved_qty_for_for_sub_assembly()
+
 	def delete_draft_work_order(self):
 		for d in frappe.get_all(
 			"Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)}
@@ -809,7 +815,11 @@
 
 			bom_data = []
 
-			warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
+			warehouse = (
+				(self.sub_assembly_warehouse or row.warehouse)
+				if self.skip_available_sub_assembly_item
+				else None
+			)
 			get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
 			self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
 			sub_assembly_items_store.extend(bom_data)
@@ -831,7 +841,7 @@
 		for data in bom_data:
 			data.qty = data.stock_qty
 			data.production_plan_item = row.name
-			data.fg_warehouse = row.warehouse
+			data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse
 			data.schedule_date = row.planned_start_date
 			data.type_of_manufacturing = manufacturing_type or (
 				"Subcontract" if data.is_sub_contracted_item else "In House"
@@ -1637,8 +1647,8 @@
 
 	query = query.run()
 
-	if not query:
-		return 0.0
+	if not query or query[0][0] is None:
+		return None
 
 	reserved_qty_for_production_plan = flt(query[0][0])
 
@@ -1780,3 +1790,29 @@
 		query = query.offset(start)
 
 	return query.run()
+
+
+def get_reserved_qty_for_sub_assembly(item_code, warehouse):
+	table = frappe.qb.DocType("Production Plan")
+	child = frappe.qb.DocType("Production Plan Sub Assembly Item")
+
+	query = (
+		frappe.qb.from_(table)
+		.inner_join(child)
+		.on(table.name == child.parent)
+		.select(Sum(child.qty - IfNull(child.wo_produced_qty, 0)))
+		.where(
+			(table.docstatus == 1)
+			& (child.production_item == item_code)
+			& (child.fg_warehouse == warehouse)
+			& (table.status.notin(["Completed", "Closed"]))
+		)
+	)
+
+	query = query.run()
+
+	if not query or query[0][0] is None:
+		return None
+
+	qty = flt(query[0][0])
+	return qty if qty > 0 else 0.0
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index d414988..e9c6ee3 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1042,13 +1042,14 @@
 		after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
 
 		self.assertEqual(after_qty - before_qty, 1)
-
 		pln = frappe.get_doc("Production Plan", pln.name)
 		pln.cancel()
 
 		bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
 		after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
 
+		pln.reload()
+		self.assertEqual(pln.docstatus, 2)
 		self.assertEqual(after_qty, before_qty)
 
 	def test_resered_qty_for_production_plan_for_work_order(self):
@@ -1359,6 +1360,93 @@
 			if row.item_code == "ChildPart2 For SUB Test":
 				self.assertEqual(row.quantity, 2)
 
+	def test_reserve_sub_assembly_items(self):
+		from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+		from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+		bom_tree = {
+			"Fininshed Goods Bicycle": {
+				"Frame Assembly": {"Frame": {}},
+				"Chain Assembly": {"Chain": {}},
+			}
+		}
+		parent_bom = create_nested_bom(bom_tree, prefix="")
+
+		warehouse = "_Test Warehouse - _TC"
+		company = "_Test Company"
+
+		sub_assembly_warehouse = create_warehouse("SUB ASSEMBLY WH", company=company)
+
+		for item_code in ["Frame", "Chain"]:
+			make_stock_entry(item_code=item_code, target=warehouse, qty=2, basic_rate=100)
+
+		before_qty = flt(
+			frappe.db.get_value(
+				"Bin",
+				{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
+				"reserved_qty_for_production_plan",
+			)
+		)
+
+		plan = create_production_plan(
+			item_code=parent_bom.item,
+			planned_qty=2,
+			ignore_existing_ordered_qty=1,
+			do_not_submit=1,
+			skip_available_sub_assembly_item=1,
+			warehouse=warehouse,
+			sub_assembly_warehouse=sub_assembly_warehouse,
+		)
+
+		plan.get_sub_assembly_items()
+		plan.submit()
+
+		after_qty = flt(
+			frappe.db.get_value(
+				"Bin",
+				{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
+				"reserved_qty_for_production_plan",
+			)
+		)
+
+		self.assertEqual(after_qty, before_qty + 2)
+
+		plan.make_work_order()
+		work_orders = frappe.get_all(
+			"Work Order",
+			fields=["name", "production_item"],
+			filters={"production_plan": plan.name},
+			order_by="creation desc",
+		)
+
+		for d in work_orders:
+			wo_doc = frappe.get_doc("Work Order", d.name)
+			wo_doc.skip_transfer = 1
+			wo_doc.from_wip_warehouse = 1
+
+			wo_doc.wip_warehouse = (
+				warehouse
+				if d.production_item in ["Frame Assembly", "Chain Assembly"]
+				else sub_assembly_warehouse
+			)
+
+			wo_doc.submit()
+
+			if d.production_item == "Frame Assembly":
+				self.assertEqual(wo_doc.fg_warehouse, sub_assembly_warehouse)
+				se_doc = frappe.get_doc(make_se_from_wo(wo_doc.name, "Manufacture", 2))
+				se_doc.submit()
+
+		after_qty = flt(
+			frappe.db.get_value(
+				"Bin",
+				{"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
+				"reserved_qty_for_production_plan",
+			)
+		)
+
+		self.assertEqual(after_qty, before_qty)
+
 
 def create_production_plan(**args):
 	"""
@@ -1379,6 +1467,7 @@
 			"ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0,
 			"get_items_from": "Sales Order",
 			"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
+			"sub_assembly_warehouse": args.sub_assembly_warehouse,
 		}
 	)
 
diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
index fde0404..aff740b 100644
--- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
+++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
@@ -17,11 +17,10 @@
   "type_of_manufacturing",
   "supplier",
   "work_order_details_section",
-  "work_order",
+  "wo_produced_qty",
   "purchase_order",
   "production_plan_item",
   "column_break_7",
-  "produced_qty",
   "received_qty",
   "indent",
   "section_break_19",
@@ -53,13 +52,6 @@
    "label": "Reference"
   },
   {
-   "fieldname": "work_order",
-   "fieldtype": "Link",
-   "label": "Work Order",
-   "options": "Work Order",
-   "read_only": 1
-  },
-  {
    "fieldname": "column_break_7",
    "fieldtype": "Column Break"
   },
@@ -81,7 +73,8 @@
   {
    "fieldname": "received_qty",
    "fieldtype": "Float",
-   "label": "Received Qty"
+   "label": "Received Qty",
+   "read_only": 1
   },
   {
    "fieldname": "bom_no",
@@ -162,12 +155,6 @@
    "options": "Warehouse"
   },
   {
-   "fieldname": "produced_qty",
-   "fieldtype": "Data",
-   "label": "Produced Quantity",
-   "read_only": 1
-  },
-  {
    "default": "In House",
    "fieldname": "type_of_manufacturing",
    "fieldtype": "Select",
@@ -209,12 +196,18 @@
    "label": "Projected Qty",
    "no_copy": 1,
    "read_only": 1
+  },
+  {
+   "fieldname": "wo_produced_qty",
+   "fieldtype": "Float",
+   "label": "Produced Qty",
+   "read_only": 1
   }
  ],
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-05-22 17:52:34.708879",
+ "modified": "2023-11-03 13:33:42.959387",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Production Plan Sub Assembly Item",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 58945bb..d9cc212 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -710,7 +710,7 @@
 		return new Promise((resolve, reject) => {
 			frappe.prompt({
 				fieldtype: 'Float',
-				label: __('Qty for {0}', [purpose]),
+				label: __('Qty for {0}', [__(purpose)]),
 				fieldname: 'qty',
 				description: __('Max: {0}', [max]),
 				default: max
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index f9fddcb..36a0cae 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -293,6 +293,7 @@
 				update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
 
 		if self.production_plan:
+			self.set_produced_qty_for_sub_assembly_item()
 			self.update_production_plan_status()
 
 	def get_transferred_or_manufactured_qty(self, purpose):
@@ -569,16 +570,49 @@
 			)
 
 	def update_planned_qty(self):
+		from erpnext.manufacturing.doctype.production_plan.production_plan import (
+			get_reserved_qty_for_sub_assembly,
+		)
+
+		qty_dict = {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)}
+
+		if self.production_plan_sub_assembly_item and self.production_plan:
+			qty_dict["reserved_qty_for_production_plan"] = get_reserved_qty_for_sub_assembly(
+				self.production_item, self.fg_warehouse
+			)
+
 		update_bin_qty(
 			self.production_item,
 			self.fg_warehouse,
-			{"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)},
+			qty_dict,
 		)
 
 		if self.material_request:
 			mr_obj = frappe.get_doc("Material Request", self.material_request)
 			mr_obj.update_requested_qty([self.material_request_item])
 
+	def set_produced_qty_for_sub_assembly_item(self):
+		table = frappe.qb.DocType("Work Order")
+
+		query = (
+			frappe.qb.from_(table)
+			.select(Sum(table.produced_qty))
+			.where(
+				(table.production_plan == self.production_plan)
+				& (table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item)
+				& (table.docstatus == 1)
+			)
+		).run()
+
+		produced_qty = flt(query[0][0]) if query else 0
+
+		frappe.db.set_value(
+			"Production Plan Sub Assembly Item",
+			self.production_plan_sub_assembly_item,
+			"wo_produced_qty",
+			produced_qty,
+		)
+
 	def update_ordered_qty(self):
 		if (
 			self.production_plan
diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js
index fd2b6a4..79fd2eb 100644
--- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js
+++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js
@@ -3,10 +3,10 @@
 
 frappe.ui.form.on('Quality Procedure', {
 	refresh: function(frm) {
-		frm.set_query("procedure","processes", (frm) =>{
+		frm.set_query('procedure', 'processes', (frm) =>{
 			return {
 				filters: {
-					name: ["not in", [frm.parent_quality_procedure, frm.name]]
+					name: ['not in', [frm.parent_quality_procedure, frm.name]]
 				}
 			};
 		});
@@ -14,7 +14,8 @@
 		frm.set_query('parent_quality_procedure', function(){
 			return {
 				filters: {
-					is_group: 1
+					is_group: 1,
+					name: ['!=', frm.doc.name]
 				}
 			};
 		});
diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
index e860408..6834abc 100644
--- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
+++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py
@@ -16,16 +16,13 @@
 	def on_update(self):
 		NestedSet.on_update(self)
 		self.set_parent()
+		self.remove_parent_from_old_child()
+		self.add_child_to_parent()
+		self.remove_child_from_old_parent()
 
 	def after_insert(self):
 		self.set_parent()
-
-		# add child to parent if missing
-		if self.parent_quality_procedure:
-			parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure)
-			if not [d for d in parent.processes if d.procedure == self.name]:
-				parent.append("processes", {"procedure": self.name, "process_description": self.name})
-				parent.save()
+		self.add_child_to_parent()
 
 	def on_trash(self):
 		# clear from child table (sub procedures)
@@ -36,15 +33,6 @@
 		)
 		NestedSet.on_trash(self, allow_root_deletion=True)
 
-	def set_parent(self):
-		for process in self.processes:
-			# Set parent for only those children who don't have a parent
-			has_parent = frappe.db.get_value(
-				"Quality Procedure", process.procedure, "parent_quality_procedure"
-			)
-			if not has_parent and process.procedure:
-				frappe.db.set_value(self.doctype, process.procedure, "parent_quality_procedure", self.name)
-
 	def check_for_incorrect_child(self):
 		for process in self.processes:
 			if process.procedure:
@@ -61,6 +49,48 @@
 						title=_("Invalid Child Procedure"),
 					)
 
+	def set_parent(self):
+		"""Set `Parent Procedure` in `Child Procedures`"""
+
+		for process in self.processes:
+			if process.procedure:
+				if not frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure"):
+					frappe.db.set_value(
+						"Quality Procedure", process.procedure, "parent_quality_procedure", self.name
+					)
+
+	def remove_parent_from_old_child(self):
+		"""Remove `Parent Procedure` from `Old Child Procedures`"""
+
+		if old_doc := self.get_doc_before_save():
+			if old_child_procedures := set([d.procedure for d in old_doc.processes if d.procedure]):
+				current_child_procedures = set([d.procedure for d in self.processes if d.procedure])
+
+				if removed_child_procedures := list(old_child_procedures.difference(current_child_procedures)):
+					for child_procedure in removed_child_procedures:
+						frappe.db.set_value("Quality Procedure", child_procedure, "parent_quality_procedure", None)
+
+	def add_child_to_parent(self):
+		"""Add `Child Procedure` to `Parent Procedure`"""
+
+		if self.parent_quality_procedure:
+			parent = frappe.get_doc("Quality Procedure", self.parent_quality_procedure)
+			if not [d for d in parent.processes if d.procedure == self.name]:
+				parent.append("processes", {"procedure": self.name, "process_description": self.name})
+				parent.save()
+
+	def remove_child_from_old_parent(self):
+		"""Remove `Child Procedure` from `Old Parent Procedure`"""
+
+		if old_doc := self.get_doc_before_save():
+			if old_parent := old_doc.parent_quality_procedure:
+				if self.parent_quality_procedure != old_parent:
+					parent = frappe.get_doc("Quality Procedure", old_parent)
+					for process in parent.processes:
+						if process.procedure == self.name:
+							parent.remove(process)
+					parent.save()
+
 
 @frappe.whitelist()
 def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=False):
diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
index 04e8211..467186d 100644
--- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
+++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
@@ -1,56 +1,107 @@
 # Copyright (c) 2018, Frappe and Contributors
 # See license.txt
 
-import unittest
-
 import frappe
+from frappe.tests.utils import FrappeTestCase
 
 from .quality_procedure import add_node
 
 
-class TestQualityProcedure(unittest.TestCase):
+class TestQualityProcedure(FrappeTestCase):
 	def test_add_node(self):
-		try:
-			procedure = frappe.get_doc(
-				dict(
-					doctype="Quality Procedure",
-					quality_procedure_name="Test Procedure 1",
-					processes=[dict(process_description="Test Step 1")],
-				)
-			).insert()
-
-			frappe.local.form_dict = frappe._dict(
-				doctype="Quality Procedure",
-				quality_procedure_name="Test Child 1",
-				parent_quality_procedure=procedure.name,
-				cmd="test",
-				is_root="false",
-			)
-			node = add_node()
-
-			procedure.reload()
-
-			self.assertEqual(procedure.is_group, 1)
-
-			# child row created
-			self.assertTrue([d for d in procedure.processes if d.procedure == node.name])
-
-			node.delete()
-			procedure.reload()
-
-			# child unset
-			self.assertFalse([d for d in procedure.processes if d.name == node.name])
-
-		finally:
-			procedure.delete()
-
-
-def create_procedure():
-	return frappe.get_doc(
-		dict(
-			doctype="Quality Procedure",
-			quality_procedure_name="Test Procedure 1",
-			is_group=1,
-			processes=[dict(process_description="Test Step 1")],
+		procedure = create_procedure(
+			{
+				"quality_procedure_name": "Test Procedure 1",
+				"is_group": 1,
+				"processes": [dict(process_description="Test Step 1")],
+			}
 		)
-	).insert()
+
+		frappe.local.form_dict = frappe._dict(
+			doctype="Quality Procedure",
+			quality_procedure_name="Test Child 1",
+			parent_quality_procedure=procedure.name,
+			cmd="test",
+			is_root="false",
+		)
+		node = add_node()
+
+		procedure.reload()
+
+		self.assertEqual(procedure.is_group, 1)
+
+		# child row created
+		self.assertTrue([d for d in procedure.processes if d.procedure == node.name])
+
+		node.delete()
+		procedure.reload()
+
+		# child unset
+		self.assertFalse([d for d in procedure.processes if d.name == node.name])
+
+	def test_remove_parent_from_old_child(self):
+		child_qp = create_procedure(
+			{
+				"quality_procedure_name": "Test Child 1",
+				"is_group": 0,
+			}
+		)
+		group_qp = create_procedure(
+			{
+				"quality_procedure_name": "Test Group",
+				"is_group": 1,
+				"processes": [dict(procedure=child_qp.name)],
+			}
+		)
+
+		child_qp.reload()
+		self.assertEqual(child_qp.parent_quality_procedure, group_qp.name)
+
+		group_qp.reload()
+		del group_qp.processes[0]
+		group_qp.save()
+
+		child_qp.reload()
+		self.assertEqual(child_qp.parent_quality_procedure, None)
+
+	def remove_child_from_old_parent(self):
+		child_qp = create_procedure(
+			{
+				"quality_procedure_name": "Test Child 1",
+				"is_group": 0,
+			}
+		)
+		group_qp = create_procedure(
+			{
+				"quality_procedure_name": "Test Group",
+				"is_group": 1,
+				"processes": [dict(procedure=child_qp.name)],
+			}
+		)
+
+		group_qp.reload()
+		self.assertTrue([d for d in group_qp.processes if d.procedure == child_qp.name])
+
+		child_qp.reload()
+		self.assertEqual(child_qp.parent_quality_procedure, group_qp.name)
+
+		child_qp.parent_quality_procedure = None
+		child_qp.save()
+
+		group_qp.reload()
+		self.assertFalse([d for d in group_qp.processes if d.procedure == child_qp.name])
+
+
+def create_procedure(kwargs=None):
+	kwargs = frappe._dict(kwargs or {})
+
+	doc = frappe.new_doc("Quality Procedure")
+	doc.quality_procedure_name = kwargs.quality_procedure_name or "_Test Procedure"
+	doc.is_group = kwargs.is_group or 0
+
+	for process in kwargs.processes or []:
+		doc.append("processes", process)
+
+	doc.insert()
+
+	return doc
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index df466ed..8b2e5cf 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -34,10 +34,15 @@
 			get_reserved_qty_for_production_plan,
 		)
 
-		self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
+		reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
 			self.item_code, self.warehouse
 		)
 
+		if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
+			return
+
+		self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
+
 		self.db_set(
 			"reserved_qty_for_production_plan",
 			flt(self.reserved_qty_for_production_plan),
@@ -48,6 +53,29 @@
 			self.set_projected_qty()
 			self.db_set("projected_qty", self.projected_qty, update_modified=True)
 
+	def update_reserved_qty_for_for_sub_assembly(self):
+		from erpnext.manufacturing.doctype.production_plan.production_plan import (
+			get_reserved_qty_for_sub_assembly,
+		)
+
+		reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly(
+			self.item_code, self.warehouse
+		)
+
+		if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
+			return
+
+		self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
+		self.set_projected_qty()
+
+		self.db_set(
+			{
+				"projected_qty": self.projected_qty,
+				"reserved_qty_for_production_plan": flt(self.reserved_qty_for_production_plan),
+			},
+			update_modified=True,
+		)
+
 	def update_reserved_qty_for_production(self):
 		"""Update qty reserved for production from Production Item tables
 		in open work orders"""
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 1eecf6d..137c352 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -1029,6 +1029,7 @@
 
 		dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-3)
 		si1 = make_sales_invoice(dn1.name)
+		si1.update_billed_amount_in_delivery_note = True
 		si1.insert()
 		si1.submit()
 		dn1.reload()
@@ -1037,6 +1038,7 @@
 
 		dn2 = create_delivery_note(is_return=1, return_against=dn.name, qty=-4)
 		si2 = make_sales_invoice(dn2.name)
+		si2.update_billed_amount_in_delivery_note = True
 		si2.insert()
 		si2.submit()
 		dn2.reload()