Update Items in Submit state can add new row (#15644)

* Update Items in Sumbit state can add new row

In Sales Order and Purchase Order when docstatus is submitted user can
use Update Item btn to add new child Items

* Remove unused code line

* Remove blocking db save thread line

* Remove Company as not standard Field in Purchase Order Item

* Minor indentation fix

* Add Unit Test, add new row in update_child_qty_rate

* Codacy fix
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index a505e49..4b07895 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -42,6 +42,7 @@
 					frm: frm,
 					child_docname: "items",
 					child_doctype: "Purchase Order Detail",
+					cannot_add_row: false,
 				})
 			});
 		}
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 86ceb2e..df48d20 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -6,6 +6,7 @@
 import json
 from frappe import _, throw
 from frappe.utils import today, flt, cint, fmt_money, formatdate, getdate, add_days, add_months, get_last_day, nowdate
+from erpnext.stock.get_item_details import get_conversion_factor
 from erpnext.setup.utils import get_exchange_rate
 from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency
 from erpnext.utilities.transaction_base import TransactionBase
@@ -1067,24 +1068,68 @@
 	}
 	return info
 
+def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code):
+	"""
+	Returns a Sales Order Item child item containing the default values
+	"""
+	p_doctype = frappe.get_doc(parent_doctype, parent_doctype_name)
+	child_item = frappe.new_doc('Sales Order Item', p_doctype, child_docname)
+	item = frappe.get_doc("Item", item_code)
+	child_item.item_code = item.item_code
+	child_item.item_name = item.item_name
+	child_item.description = item.description
+	child_item.reqd_by_date = p_doctype.delivery_date
+	child_item.uom = item.stock_uom
+	child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0
+	return child_item
+
+
+def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code):
+	"""
+	Returns a Purchase Order Item child item containing the default values
+	"""
+	p_doctype = frappe.get_doc(parent_doctype, parent_doctype_name)
+	child_item = frappe.new_doc('Purchase Order Item', p_doctype, child_docname)
+	item = frappe.get_doc("Item", item_code)
+	child_item.item_code = item.item_code
+	child_item.item_name = item.item_name
+	child_item.description = item.description
+	child_item.schedule_date = p_doctype.schedule_date
+	child_item.uom = item.stock_uom
+	child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0
+	child_item.base_rate = 1 # Initiallize value will update in parent validation
+	child_item.base_amount = 1 # Initiallize value will update in parent validation
+	return child_item
+
 
 @frappe.whitelist()
-def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name):
+def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
 	data = json.loads(trans_items)
-	for d in data:
-		child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname"))
-		child_item.qty = flt(d.get("qty"))
 
+	for d in data:
+		new_child_flag = False
+		if not d.get("docname"):
+			new_child_flag = True
+			if parent_doctype == "Sales Order":
+				child_item  = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code"))
+			if parent_doctype == "Purchase Order":
+				child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code"))
+		else:
+			child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname"))
+
+		child_item.qty = flt(d.get("qty"))
 		if child_item.billed_amt > (flt(d.get("rate")) * flt(d.get("qty"))):
 			frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.")
 						 .format(child_item.idx, child_item.item_code))
 		else:
 			child_item.rate = flt(d.get("rate"))
 		child_item.flags.ignore_validate_update_after_submit = True
-		child_item.save()
+		if new_child_flag:
+			child_item.insert()
+		else:
+			child_item.save()
 
 	p_doctype = frappe.get_doc(parent_doctype, parent_doctype_name)
-
 	p_doctype.flags.ignore_validate_update_after_submit = True
 	p_doctype.set_qty_as_per_stock_uom()
 	p_doctype.calculate_taxes_and_totals()
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index e293321..391323d 100644
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -406,15 +406,19 @@
 
 erpnext.utils.update_child_items = function(opts) {
 	const frm = opts.frm;
-
+	const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row;
+	const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname;
 	this.data = [];
 	const dialog = new frappe.ui.Dialog({
 		title: __("Update Items"),
 		fields: [
 			{fieldtype:'Section Break', label: __('Items')},
 			{
-				fieldname: "trans_items", fieldtype: "Table", cannot_add_rows: true,
-				in_place_edit: true, data: this.data,
+				fieldname: "trans_items",
+				fieldtype: "Table",
+				cannot_add_rows: cannot_add_row,
+				in_place_edit: true,
+				data: this.data,
 				get_data: () => {
 					return this.data;
 				},
@@ -450,10 +454,12 @@
 			const trans_items = this.get_values()["trans_items"];
 			frappe.call({
 				method: 'erpnext.controllers.accounts_controller.update_child_qty_rate',
+				freeze: true,
 				args: {
 					'parent_doctype': frm.doc.doctype,
 					'trans_items': trans_items,
-					'parent_doctype_name': frm.doc.name
+					'parent_doctype_name': frm.doc.name,
+					'child_docname': child_docname
 				},
 				callback: function() {
 					frm.reload_doc();
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 54d7654..18f28a8 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -25,6 +25,7 @@
 					frm: frm,
 					child_docname: "items",
 					child_doctype: "Sales Order Detail",
+					cannot_add_row: false,
 				})
 			});
 		}
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 65e91bc..64d543a 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -12,6 +12,8 @@
 from erpnext.controllers.accounts_controller import update_child_qty_rate
 import json
 from erpnext.selling.doctype.sales_order.sales_order import make_raw_material_request
+
+
 class TestSalesOrder(unittest.TestCase):
 	def tearDown(self):
 		frappe.set_user("Administrator")
@@ -268,6 +270,22 @@
 		self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"),
 			existing_reserved_qty_item2 + 20)
 
+	def test_add_new_item_in_update_child_qty_rate(self):
+		so = make_sales_order(item_code= "_Test Item", qty=4)
+		create_dn_against_so(so.name, 4)
+		make_sales_invoice(so.name)
+
+		trans_item = json.dumps([{'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 7}])
+		update_child_qty_rate('Sales Order', trans_item, so.name)
+
+		so.reload()
+		self.assertEqual(so.get("items")[-1].item_code, '_Test Item 2')
+		self.assertEqual(so.get("items")[-1].rate, 200)
+		self.assertEqual(so.get("items")[-1].qty, 7)
+		self.assertEqual(so.get("items")[-1].amount, 1400)
+		self.assertEqual(so.status, 'To Deliver and Bill')
+
+
 	def test_update_child_qty_rate(self):
 		so = make_sales_order(item_code= "_Test Item", qty=4)
 		create_dn_against_so(so.name, 4)
@@ -760,7 +778,7 @@
 		})
 
 	so.delivery_date = add_days(so.transaction_date, 10)
- 
+
 	if not args.do_not_save:
 		so.insert()
 		if not args.do_not_submit: