Merge pull request #38644 from barredterra/fix-attribute-error

fix: attribute error
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
index dda0ec7..1bce1a6 100644
--- a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py
@@ -22,7 +22,7 @@
 		self.create_item()
 		self.update_repost_settings()
 
-	def teadDown(self):
+	def tearDown(self):
 		frappe.db.rollback()
 
 	def update_repost_settings(self):
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 1b8f66c..abcea44 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -292,6 +292,7 @@
 	def on_trash(self):
 		self._remove_references_in_repost_doctypes()
 		self._remove_references_in_unreconcile()
+		self.remove_serial_and_batch_bundle()
 
 		# delete sl and gl entries on deletion of transaction
 		if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
@@ -307,6 +308,15 @@
 				(self.doctype, self.name),
 			)
 
+	def remove_serial_and_batch_bundle(self):
+		bundles = frappe.get_all(
+			"Serial and Batch Bundle",
+			filters={"voucher_type": self.doctype, "voucher_no": self.name, "docstatus": ("!=", 1)},
+		)
+
+		for bundle in bundles:
+			frappe.delete_doc("Serial and Batch Bundle", bundle.name)
+
 	def validate_deferred_income_expense_account(self):
 		field_map = {
 			"Sales Invoice": "deferred_revenue_account",
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 0860d9c..3ed7fc7 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -36,14 +36,14 @@
 
 				// no idea where me is coming from
 				if(this.frm.get_field('shipping_address')) {
-					this.frm.set_query("shipping_address", function() {
-						if(me.frm.doc.customer) {
+					this.frm.set_query("shipping_address", () => {
+						if(this.frm.doc.customer) {
 							return {
 								query: 'frappe.contacts.doctype.address.address.address_query',
-								filters: { link_doctype: 'Customer', link_name: me.frm.doc.customer }
+								filters: { link_doctype: 'Customer', link_name: this.frm.doc.customer }
 							};
 						} else
-							return erpnext.queries.company_address_query(me.frm.doc)
+							return erpnext.queries.company_address_query(this.frm.doc)
 					});
 				}
 			}
diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py
index da7edbf..5a60d2f 100644
--- a/erpnext/startup/leaderboard.py
+++ b/erpnext/startup/leaderboard.py
@@ -1,5 +1,5 @@
 import frappe
-from frappe.utils import cint
+from frappe.utils.deprecations import deprecated
 
 
 def get_leaderboards():
@@ -54,12 +54,13 @@
 
 @frappe.whitelist()
 def get_all_customers(date_range, company, field, limit=None):
+	filters = [["docstatus", "=", "1"], ["company", "=", company]]
+	from_date, to_date = parse_date_range(date_range)
 	if field == "outstanding_amount":
-		filters = [["docstatus", "=", "1"], ["company", "=", company]]
-		if date_range:
-			date_range = frappe.parse_json(date_range)
-			filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]])
-		return frappe.db.get_all(
+		if from_date and to_date:
+			filters.append(["posting_date", "between", [from_date, to_date]])
+
+		return frappe.get_list(
 			"Sales Invoice",
 			fields=["customer as name", "sum(outstanding_amount) as value"],
 			filters=filters,
@@ -69,26 +70,20 @@
 		)
 	else:
 		if field == "total_sales_amount":
-			select_field = "sum(so_item.base_net_amount)"
+			select_field = "base_net_total"
 		elif field == "total_qty_sold":
-			select_field = "sum(so_item.stock_qty)"
+			select_field = "total_qty"
 
-		date_condition = get_date_condition(date_range, "so.transaction_date")
+		if from_date and to_date:
+			filters.append(["transaction_date", "between", [from_date, to_date]])
 
-		return frappe.db.sql(
-			"""
-			select so.customer as name, {0} as value
-			FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item
-				ON so.name = so_item.parent
-			where so.docstatus = 1 {1} and so.company = %s
-			group by so.customer
-			order by value DESC
-			limit %s
-		""".format(
-				select_field, date_condition
-			),
-			(company, cint(limit)),
-			as_dict=1,
+		return frappe.get_list(
+			"Sales Order",
+			fields=["customer as name", f"sum({select_field}) as value"],
+			filters=filters,
+			group_by="customer",
+			order_by="value desc",
+			limit=limit,
 		)
 
 
@@ -96,55 +91,58 @@
 def get_all_items(date_range, company, field, limit=None):
 	if field in ("available_stock_qty", "available_stock_value"):
 		select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
-		return frappe.db.get_all(
+		results = frappe.db.get_all(
 			"Bin",
 			fields=["item_code as name", "{0} as value".format(select_field)],
 			group_by="item_code",
 			order_by="value desc",
 			limit=limit,
 		)
+		readable_active_items = set(frappe.get_list("Item", filters={"disabled": 0}, pluck="name"))
+		return [item for item in results if item["name"] in readable_active_items]
 	else:
 		if field == "total_sales_amount":
-			select_field = "sum(order_item.base_net_amount)"
+			select_field = "base_net_amount"
 			select_doctype = "Sales Order"
 		elif field == "total_purchase_amount":
-			select_field = "sum(order_item.base_net_amount)"
+			select_field = "base_net_amount"
 			select_doctype = "Purchase Order"
 		elif field == "total_qty_sold":
-			select_field = "sum(order_item.stock_qty)"
+			select_field = "stock_qty"
 			select_doctype = "Sales Order"
 		elif field == "total_qty_purchased":
-			select_field = "sum(order_item.stock_qty)"
+			select_field = "stock_qty"
 			select_doctype = "Purchase Order"
 
-		date_condition = get_date_condition(date_range, "sales_order.transaction_date")
+		filters = [["docstatus", "=", "1"], ["company", "=", company]]
+		from_date, to_date = parse_date_range(date_range)
+		if from_date and to_date:
+			filters.append(["transaction_date", "between", [from_date, to_date]])
 
-		return frappe.db.sql(
-			"""
-			select order_item.item_code as name, {0} as value
-			from `tab{1}` sales_order join `tab{1} Item` as order_item
-				on sales_order.name = order_item.parent
-			where sales_order.docstatus = 1
-				and sales_order.company = %s {2}
-			group by order_item.item_code
-			order by value desc
-			limit %s
-		""".format(
-				select_field, select_doctype, date_condition
-			),
-			(company, cint(limit)),
-			as_dict=1,
-		)  # nosec
+		child_doctype = f"{select_doctype} Item"
+		return frappe.get_list(
+			select_doctype,
+			fields=[
+				f"`tab{child_doctype}`.item_code as name",
+				f"sum(`tab{child_doctype}`.{select_field}) as value",
+			],
+			filters=filters,
+			order_by="value desc",
+			group_by=f"`tab{child_doctype}`.item_code",
+			limit=limit,
+		)
 
 
 @frappe.whitelist()
 def get_all_suppliers(date_range, company, field, limit=None):
+	filters = [["docstatus", "=", "1"], ["company", "=", company]]
+	from_date, to_date = parse_date_range(date_range)
+
 	if field == "outstanding_amount":
-		filters = [["docstatus", "=", "1"], ["company", "=", company]]
-		if date_range:
-			date_range = frappe.parse_json(date_range)
-			filters.append(["posting_date", "between", [date_range[0], date_range[1]]])
-		return frappe.db.get_all(
+		if from_date and to_date:
+			filters.append(["posting_date", "between", [from_date, to_date]])
+
+		return frappe.get_list(
 			"Purchase Invoice",
 			fields=["supplier as name", "sum(outstanding_amount) as value"],
 			filters=filters,
@@ -154,48 +152,40 @@
 		)
 	else:
 		if field == "total_purchase_amount":
-			select_field = "sum(purchase_order_item.base_net_amount)"
+			select_field = "base_net_total"
 		elif field == "total_qty_purchased":
-			select_field = "sum(purchase_order_item.stock_qty)"
+			select_field = "total_qty"
 
-		date_condition = get_date_condition(date_range, "purchase_order.modified")
+		if from_date and to_date:
+			filters.append(["transaction_date", "between", [from_date, to_date]])
 
-		return frappe.db.sql(
-			"""
-			select purchase_order.supplier as name, {0} as value
-			FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item`
-				as purchase_order_item ON purchase_order.name = purchase_order_item.parent
-			where
-				purchase_order.docstatus = 1
-				{1}
-				and  purchase_order.company = %s
-			group by purchase_order.supplier
-			order by value DESC
-			limit %s""".format(
-				select_field, date_condition
-			),
-			(company, cint(limit)),
-			as_dict=1,
-		)  # nosec
+		return frappe.get_list(
+			"Purchase Order",
+			fields=["supplier as name", f"sum({select_field}) as value"],
+			filters=filters,
+			group_by="supplier",
+			order_by="value desc",
+			limit=limit,
+		)
 
 
 @frappe.whitelist()
 def get_all_sales_partner(date_range, company, field, limit=None):
 	if field == "total_sales_amount":
-		select_field = "sum(`base_net_total`)"
+		select_field = "base_net_total"
 	elif field == "total_commission":
-		select_field = "sum(`total_commission`)"
+		select_field = "total_commission"
 
-	filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company}
-	if date_range:
-		date_range = frappe.parse_json(date_range)
-		filters["transaction_date"] = ["between", [date_range[0], date_range[1]]]
+	filters = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]]
+	from_date, to_date = parse_date_range(date_range)
+	if from_date and to_date:
+		filters.append(["transaction_date", "between", [from_date, to_date]])
 
 	return frappe.get_list(
 		"Sales Order",
 		fields=[
-			"`sales_partner` as name",
-			"{} as value".format(select_field),
+			"sales_partner as name",
+			f"sum({select_field}) as value",
 		],
 		filters=filters,
 		group_by="sales_partner",
@@ -206,27 +196,29 @@
 
 @frappe.whitelist()
 def get_all_sales_person(date_range, company, field=None, limit=0):
-	date_condition = get_date_condition(date_range, "sales_order.transaction_date")
+	filters = [
+		["docstatus", "=", "1"],
+		["company", "=", company],
+		["Sales Team", "sales_person", "is", "set"],
+	]
+	from_date, to_date = parse_date_range(date_range)
+	if from_date and to_date:
+		filters.append(["transaction_date", "between", [from_date, to_date]])
 
-	return frappe.db.sql(
-		"""
-		select sales_team.sales_person as name, sum(sales_order.base_net_total) as value
-		from `tabSales Order` as sales_order join `tabSales Team` as sales_team
-			on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order'
-		where sales_order.docstatus = 1
-			and sales_order.company = %s
-			{date_condition}
-		group by sales_team.sales_person
-		order by value DESC
-		limit %s
-	""".format(
-			date_condition=date_condition
-		),
-		(company, cint(limit)),
-		as_dict=1,
+	return frappe.get_list(
+		"Sales Order",
+		fields=[
+			"`tabSales Team`.sales_person as name",
+			"sum(`tabSales Team`.allocated_amount) as value",
+		],
+		filters=filters,
+		group_by="`tabSales Team`.sales_person",
+		order_by="value desc",
+		limit=limit,
 	)
 
 
+@deprecated
 def get_date_condition(date_range, field):
 	date_condition = ""
 	if date_range:
@@ -236,3 +228,11 @@
 			field, frappe.db.escape(from_date), frappe.db.escape(to_date)
 		)
 	return date_condition
+
+
+def parse_date_range(date_range):
+	if date_range:
+		date_range = frappe.parse_json(date_range)
+		return date_range[0], date_range[1]
+
+	return None, None
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js
index cda4445..9f01ee9 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js
@@ -121,7 +121,7 @@
 			frappe.throw(__("Please attach CSV file"));
 		}
 
-		if (frm.doc.has_serial_no && !prompt_data.using_csv_file && !prompt_data.serial_nos) {
+		if (frm.doc.has_serial_no && !prompt_data.csv_file && !prompt_data.serial_nos) {
 			frappe.throw(__("Please enter serial nos"));
 		}
 	},
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index 108a2e0..6785881 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -511,6 +511,22 @@
 		serial_batches = {}
 
 		for row in self.entries:
+			if self.has_serial_no and not row.serial_no:
+				frappe.throw(
+					_("At row {0}: Serial No is mandatory for Item {1}").format(
+						bold(row.idx), bold(self.item_code)
+					),
+					title=_("Serial No is mandatory"),
+				)
+
+			if self.has_batch_no and not row.batch_no:
+				frappe.throw(
+					_("At row {0}: Batch No is mandatory for Item {1}").format(
+						bold(row.idx), bold(self.item_code)
+					),
+					title=_("Batch No is mandatory"),
+				)
+
 			if row.serial_no:
 				serial_nos.append(row.serial_no)
 
@@ -794,6 +810,9 @@
 		if index == 0:
 			has_serial_no = row[0] == "Serial No"
 			has_batch_no = row[0] == "Batch No"
+			if not has_batch_no:
+				has_batch_no = row[1] == "Batch No"
+
 			continue
 
 		if not row[0]:
@@ -810,6 +829,13 @@
 					}
 				)
 
+				batch_nos.append(
+					{
+						"batch_no": row[1],
+						"qty": row[2],
+					}
+				)
+
 			serial_nos.append(_dict)
 		elif has_batch_no:
 			batch_nos.append(
@@ -845,6 +871,9 @@
 	serial_nos_details = []
 	user = frappe.session.user
 	for serial_no in serial_nos:
+		if frappe.db.exists("Serial No", serial_no):
+			continue
+
 		serial_nos_details.append(
 			(
 				serial_no,
@@ -875,7 +904,7 @@
 
 	frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
 
-	frappe.msgprint(_("Serial Nos are created successfully"))
+	frappe.msgprint(_("Serial Nos are created successfully"), alert=True)
 
 
 def make_batch_nos(item_code, batch_nos):
@@ -886,6 +915,9 @@
 	batch_nos_details = []
 	user = frappe.session.user
 	for batch_no in batch_nos:
+		if frappe.db.exists("Batch", batch_no):
+			continue
+
 		batch_nos_details.append(
 			(batch_no, batch_no, now(), now(), user, user, item.item_code, item.item_name, item.description)
 		)
@@ -904,7 +936,7 @@
 
 	frappe.db.bulk_insert("Batch", fields=fields, values=set(batch_nos_details))
 
-	frappe.msgprint(_("Batch Nos are created successfully"))
+	frappe.msgprint(_("Batch Nos are created successfully"), alert=True)
 
 
 def parse_serial_nos(data):
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
index 0e01b20..d74d657 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
@@ -368,6 +368,58 @@
 		# Batch does not belong to serial no
 		self.assertRaises(frappe.exceptions.ValidationError, doc.save)
 
+	def test_auto_delete_draft_serial_and_batch_bundle(self):
+		serial_and_batch_code = "New Serial No Auto Delete 1"
+		make_item(
+			serial_and_batch_code,
+			{
+				"has_serial_no": 1,
+				"serial_no_series": "TEST-SER-VALL-.#####",
+				"is_stock_item": 1,
+			},
+		)
+
+		ste = make_stock_entry(
+			item_code=serial_and_batch_code,
+			target="_Test Warehouse - _TC",
+			qty=1,
+			rate=500,
+			do_not_submit=True,
+		)
+
+		serial_no = "SN-TEST-AUTO-DEL"
+		if not frappe.db.exists("Serial No", serial_no):
+			frappe.get_doc(
+				{
+					"doctype": "Serial No",
+					"serial_no": serial_no,
+					"item_code": serial_and_batch_code,
+					"company": "_Test Company",
+				}
+			).insert(ignore_permissions=True)
+
+		bundle_doc = make_serial_batch_bundle(
+			{
+				"item_code": serial_and_batch_code,
+				"warehouse": "_Test Warehouse - _TC",
+				"voucher_type": "Stock Entry",
+				"posting_date": ste.posting_date,
+				"posting_time": ste.posting_time,
+				"qty": 1,
+				"serial_nos": [serial_no],
+				"type_of_transaction": "Inward",
+				"do_not_submit": True,
+			}
+		)
+
+		bundle_doc.reload()
+		ste.items[0].serial_and_batch_bundle = bundle_doc.name
+		ste.save()
+		ste.reload()
+
+		ste.delete()
+		self.assertFalse(frappe.db.exists("Serial and Batch Bundle", bundle_doc.name))
+
 
 def get_batch_from_bundle(bundle):
 	from erpnext.stock.serial_batch_bundle import get_batch_nos
diff --git a/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json b/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json
index 09565cb..5de2c2e 100644
--- a/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json
+++ b/erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json
@@ -27,7 +27,6 @@
    "in_list_view": 1,
    "in_standard_filter": 1,
    "label": "Serial No",
-   "mandatory_depends_on": "eval:parent.has_serial_no == 1",
    "options": "Serial No",
    "search_index": 1
   },
@@ -38,7 +37,6 @@
    "in_list_view": 1,
    "in_standard_filter": 1,
    "label": "Batch No",
-   "mandatory_depends_on": "eval:parent.has_batch_no == 1",
    "options": "Batch",
    "search_index": 1
   },
@@ -122,7 +120,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2023-07-03 15:29:50.199075",
+ "modified": "2023-12-10 19:47:48.227772",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Serial and Batch Entry",