fix: parent warehouse checks in the production plan for sub-assemblies (#40150)

diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index c9c474d..667ece2 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -518,6 +518,12 @@
 	}
 });
 
+frappe.ui.form.on("Production Plan Sub Assembly Item", {
+	fg_warehouse(frm, cdt, cdn) {
+		erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "sub_assembly_items", "fg_warehouse");
+	},
+})
+
 frappe.tour['Production Plan'] = [
 	{
 		fieldname: "get_items_from",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 54c3893..84bbad5 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -421,9 +421,11 @@
    "fieldtype": "Column Break"
   },
   {
+   "description": "When a parent warehouse is chosen, the system conducts stock checks against the associated child warehouses",
    "fieldname": "sub_assembly_warehouse",
    "fieldtype": "Link",
    "label": "Sub Assembly Warehouse",
+   "mandatory_depends_on": "eval:doc.skip_available_sub_assembly_item === 1",
    "options": "Warehouse"
   },
   {
@@ -437,7 +439,7 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2024-02-11 15:42:47.642481",
+ "modified": "2024-02-27 13:34:20.692211",
  "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 517b2b0..c852f84 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -894,8 +894,8 @@
 		sub_assembly_items_store = []  # temporary store to process all subassembly items
 
 		for row in self.po_items:
-			if self.skip_available_sub_assembly_item and not row.warehouse:
-				frappe.throw(_("Row #{0}: Please select the FG Warehouse in Assembly Items").format(row.idx))
+			if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse:
+				frappe.throw(_("Row #{0}: Please select the Sub Assembly Warehouse").format(row.idx))
 
 			if not row.item_code:
 				frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
@@ -905,15 +905,24 @@
 
 			bom_data = []
 
-			warehouse = (
-				(self.sub_assembly_warehouse or row.warehouse)
-				if self.skip_available_sub_assembly_item
-				else None
-			)
+			warehouse = (self.sub_assembly_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)
 
+		if not sub_assembly_items_store and self.skip_available_sub_assembly_item:
+			message = (
+				_(
+					"As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}."
+				).format(self.sub_assembly_warehouse)
+				+ "<br><br>"
+			)
+			message += _(
+				"If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox."
+			)
+
+			frappe.msgprint(message, title=_("Note"))
+
 		if self.combine_sub_items:
 			# Combine subassembly items
 			sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store)
@@ -926,15 +935,19 @@
 
 	def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
 		"Modify bom_data, set additional details."
+		is_group_warehouse = frappe.db.get_value("Warehouse", self.sub_assembly_warehouse, "is_group")
+
 		for data in bom_data:
 			data.qty = data.stock_qty
 			data.production_plan_item = row.name
-			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"
 			)
 
+			if not is_group_warehouse:
+				data.fg_warehouse = self.sub_assembly_warehouse
+
 	def set_default_supplier_for_subcontracting_order(self):
 		items = [
 			d.production_item for d in self.sub_assembly_items if d.type_of_manufacturing == "Subcontract"
@@ -1478,7 +1491,7 @@
 	so_item_details = frappe._dict()
 
 	sub_assembly_items = {}
-	if doc.get("skip_available_sub_assembly_item"):
+	if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
 		for d in doc.get("sub_assembly_items"):
 			sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty"))
 
@@ -1690,34 +1703,37 @@
 			stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
 
 			if warehouse:
-				bin_dict = get_bin_details(d, company, for_warehouse=warehouse)
+				bin_details = get_bin_details(d, company, for_warehouse=warehouse)
 
-				if bin_dict and bin_dict[0].projected_qty > 0:
-					if bin_dict[0].projected_qty > stock_qty:
-						continue
-					else:
-						stock_qty = stock_qty - bin_dict[0].projected_qty
+				for _bin_dict in bin_details:
+					if _bin_dict.projected_qty > 0:
+						if _bin_dict.projected_qty > stock_qty:
+							stock_qty = 0
+							continue
+						else:
+							stock_qty = stock_qty - _bin_dict.projected_qty
 
-			bom_data.append(
-				frappe._dict(
-					{
-						"parent_item_code": parent_item_code,
-						"description": d.description,
-						"production_item": d.item_code,
-						"item_name": d.item_name,
-						"stock_uom": d.stock_uom,
-						"uom": d.stock_uom,
-						"bom_no": d.value,
-						"is_sub_contracted_item": d.is_sub_contracted_item,
-						"bom_level": indent,
-						"indent": indent,
-						"stock_qty": stock_qty,
-					}
+			if stock_qty > 0:
+				bom_data.append(
+					frappe._dict(
+						{
+							"parent_item_code": parent_item_code,
+							"description": d.description,
+							"production_item": d.item_code,
+							"item_name": d.item_name,
+							"stock_uom": d.stock_uom,
+							"uom": d.stock_uom,
+							"bom_no": d.value,
+							"is_sub_contracted_item": d.is_sub_contracted_item,
+							"bom_level": indent,
+							"indent": indent,
+							"stock_qty": stock_qty,
+						}
+					)
 				)
-			)
 
-			if d.value:
-				get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
+				if d.value:
+					get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
 
 
 def set_default_warehouses(row, default_warehouses):
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 53537f9..0bf3705 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1205,6 +1205,7 @@
 			ignore_existing_ordered_qty=1,
 			do_not_submit=1,
 			skip_available_sub_assembly_item=1,
+			sub_assembly_warehouse="_Test Warehouse - _TC",
 			warehouse="_Test Warehouse - _TC",
 		)
 
@@ -1338,6 +1339,7 @@
 			ignore_existing_ordered_qty=1,
 			do_not_submit=1,
 			skip_available_sub_assembly_item=1,
+			sub_assembly_warehouse="_Test Warehouse - _TC",
 			warehouse="_Test Warehouse - _TC",
 		)
 
@@ -1590,6 +1592,48 @@
 		for row in work_orders:
 			self.assertEqual(row.qty, wo_qty[row.name])
 
+	def test_parent_warehouse_for_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
+
+		parent_warehouse = "_Test Warehouse Group - _TC"
+		sub_warehouse = create_warehouse("Sub Warehouse", company="_Test Company")
+
+		fg_item = make_item(properties={"is_stock_item": 1}).name
+		sf_item = make_item(properties={"is_stock_item": 1}).name
+		rm_item = make_item(properties={"is_stock_item": 1}).name
+
+		bom_tree = {fg_item: {sf_item: {rm_item: {}}}}
+		create_nested_bom(bom_tree, prefix="")
+
+		pln = create_production_plan(
+			item_code=fg_item,
+			planned_qty=10,
+			warehouse="_Test Warehouse - _TC",
+			sub_assembly_warehouse=parent_warehouse,
+			skip_available_sub_assembly_item=1,
+			do_not_submit=1,
+			skip_getting_mr_items=1,
+		)
+
+		pln.get_sub_assembly_items()
+
+		for row in pln.sub_assembly_items:
+			self.assertFalse(row.fg_warehouse)
+			self.assertEqual(row.production_item, sf_item)
+			self.assertEqual(row.qty, 10.0)
+
+		make_stock_entry(item_code=sf_item, qty=5, target=sub_warehouse, rate=100)
+
+		pln.sub_assembly_items = []
+		pln.get_sub_assembly_items()
+
+		self.assertEqual(pln.sub_assembly_warehouse, parent_warehouse)
+		for row in pln.sub_assembly_items:
+			self.assertFalse(row.fg_warehouse)
+			self.assertEqual(row.production_item, sf_item)
+			self.assertEqual(row.qty, 5.0)
+
 
 def create_production_plan(**args):
 	"""
diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
index 0688278..78a3897 100644
--- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
+++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
@@ -11,6 +11,7 @@
   "bom_no",
   "column_break_6",
   "planned_qty",
+  "stock_uom",
   "warehouse",
   "planned_start_date",
   "section_break_9",
@@ -18,7 +19,6 @@
   "ordered_qty",
   "column_break_17",
   "description",
-  "stock_uom",
   "produced_qty",
   "reference_section",
   "sales_order",
@@ -65,6 +65,7 @@
    "width": "100px"
   },
   {
+   "columns": 1,
    "fieldname": "planned_qty",
    "fieldtype": "Float",
    "in_list_view": 1,
@@ -80,6 +81,7 @@
    "fieldtype": "Column Break"
   },
   {
+   "columns": 2,
    "fieldname": "warehouse",
    "fieldtype": "Link",
    "in_list_view": 1,
@@ -141,8 +143,10 @@
    "width": "200px"
   },
   {
+   "columns": 1,
    "fieldname": "stock_uom",
    "fieldtype": "Link",
+   "in_list_view": 1,
    "label": "UOM",
    "oldfieldname": "stock_uom",
    "oldfieldtype": "Data",
@@ -216,7 +220,7 @@
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-11-25 14:15:40.061514",
+ "modified": "2024-02-27 13:24:43.571844",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Production Plan Item",
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 aff740b..7965965 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
@@ -101,7 +101,6 @@
    "columns": 1,
    "fieldname": "bom_level",
    "fieldtype": "Int",
-   "in_list_view": 1,
    "label": "Level (BOM)",
    "read_only": 1
   },
@@ -149,8 +148,10 @@
    "label": "Indent"
   },
   {
+   "columns": 2,
    "fieldname": "fg_warehouse",
    "fieldtype": "Link",
+   "in_list_view": 1,
    "label": "Target Warehouse",
    "options": "Warehouse"
   },
@@ -170,6 +171,7 @@
    "options": "Supplier"
   },
   {
+   "columns": 1,
    "fieldname": "schedule_date",
    "fieldtype": "Datetime",
    "in_list_view": 1,
@@ -207,7 +209,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-11-03 13:33:42.959387",
+ "modified": "2024-02-27 13:45:17.422435",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Production Plan Sub Assembly Item",