fix: subassembly items linked to temporary name

Production Plan tables for po_items and sub_assembly_items are prepared
client side so both dont exist at time of first save or modifying and
hence any "links" created are invalid. This change retains temporary
name so it can be relinked server side after naming is performed.

Co-Authored-By: Marica <maricadsouza221197@gmail.com>
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index e8759f5..59ddf1f 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -2,6 +2,13 @@
 // For license information, please see license.txt
 
 frappe.ui.form.on('Production Plan', {
+
+	before_save: function(frm) {
+		// preserve temporary names on production plan item to re-link sub-assembly items
+		frm.doc.po_items.forEach(item => {
+			item.temporary_name = item.name;
+		});
+	},
 	setup: function(frm) {
 		frm.custom_make_buttons = {
 			'Work Order': 'Work Order / Subcontract PO',
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 2b6e696..b88b24f 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -32,6 +32,7 @@
 		self.set_pending_qty_in_row_without_reference()
 		self.calculate_total_planned_qty()
 		self.set_status()
+		self._rename_temporary_references()
 
 	def set_pending_qty_in_row_without_reference(self):
 		"Set Pending Qty in independent rows (not from SO or MR)."
@@ -57,6 +58,18 @@
 			if not flt(d.planned_qty):
 				frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
 
+	def _rename_temporary_references(self):
+		""" po_items and sub_assembly_items items are both constructed client side without saving.
+
+			Attempt to fix linkages by using temporary names to map final row names.
+		"""
+		new_name_map = {d.temporary_name: d.name for d in self.po_items if d.temporary_name}
+		actual_names = set(new_name_map.values())
+
+		for sub_assy in self.sub_assembly_items:
+			if sub_assy.production_plan_item not in actual_names:
+				sub_assy.production_plan_item = new_name_map.get(sub_assy.production_plan_item)
+
 	@frappe.whitelist()
 	def get_open_sales_orders(self):
 		""" Pull sales orders  which are pending to deliver based on criteria selected"""
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 6425374..ec49703 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -667,6 +667,39 @@
 		wo_doc.submit()
 		self.assertEqual(wo_doc.qty, 0.55)
 
+	def test_temporary_name_relinking(self):
+
+		pp = frappe.new_doc("Production Plan")
+
+		# this can not be unittested so mocking data that would be expected
+		# from client side.
+		for _ in range(10):
+			po_item = pp.append("po_items", {
+				"name": frappe.generate_hash(length=10),
+				"temporary_name": frappe.generate_hash(length=10),
+			})
+			pp.append("sub_assembly_items", {
+				"production_plan_item": po_item.temporary_name
+			})
+		pp._rename_temporary_references()
+
+		for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
+			self.assertEqual(po_item.name, subassy_item.production_plan_item)
+
+		# bad links should be erased
+		pp.append("sub_assembly_items", {
+			"production_plan_item": frappe.generate_hash(length=16)
+		})
+		pp._rename_temporary_references()
+		self.assertIsNone(pp.sub_assembly_items[-1].production_plan_item)
+		pp.sub_assembly_items.pop()
+
+		# reattempting on same doc shouldn't change anything
+		pp._rename_temporary_references()
+		for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
+			self.assertEqual(po_item.name, subassy_item.production_plan_item)
+
+
 def create_production_plan(**args):
 	"""
 	sales_order (obj): Sales Order Doc Object
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 f829d57..df5862f 100644
--- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
+++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json
@@ -27,7 +27,8 @@
   "material_request",
   "material_request_item",
   "product_bundle_item",
-  "item_reference"
+  "item_reference",
+  "temporary_name"
  ],
  "fields": [
   {
@@ -204,17 +205,25 @@
    "fieldtype": "Data",
    "hidden": 1,
    "label": "Item Reference"
+  },
+  {
+   "fieldname": "temporary_name",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "temporary name"
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-06-28 18:31:06.822168",
+ "modified": "2022-03-24 04:54:09.940224",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Production Plan Item",
+ "naming_rule": "Random",
  "owner": "Administrator",
  "permissions": [],
  "sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 3199912..35aac54 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -360,4 +360,4 @@
 erpnext.patches.v14_0.delete_non_profit_doctypes
 erpnext.patches.v14_0.update_employee_advance_status
 erpnext.patches.v13_0.add_cost_center_in_loans
-erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items
+erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022