feat: Filter rows to be mapped on server side mapping function

- Pass dialog selections to `make_sales_order`
- Map either original item or its alternative depending on mapping
- Only qty check for simple rows (without alternatives and not an alternative itself)
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index 6f75673..0ea424f 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -233,7 +233,9 @@
 
 	show_alternative_item_dialog() {
 		// Create a `{original item: [alternate items]}` map
-		const item_alt_map = {};
+		var me = this;
+		let item_alt_map = {};
+
 		this.frm.doc.items.filter(
 			(item) => item.is_alternative
 		).forEach((item) =>
@@ -286,7 +288,14 @@
 				},
 			],
 			primary_action: function() {
-				this.hide();
+				frappe.model.open_mapped_doc({
+					method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
+					frm: me.frm,
+					args: {
+						mapping: dialog.get_value("alternative_items")
+					}
+				});
+				dialog.hide();
 			},
 			primary_action_label: __('Continue')
 		});
@@ -297,13 +306,6 @@
 
 cur_frm.script_manager.make(erpnext.selling.QuotationController);
 
-cur_frm.cscript['Make Sales Order'] = function() {
-	frappe.model.open_mapped_doc({
-		method: "erpnext.selling.doctype.quotation.quotation.make_sales_order",
-		frm: cur_frm
-	})
-}
-
 frappe.ui.form.on("Quotation Item", "items_on_form_rendered", "packed_items_on_form_rendered", function(frm, cdt, cdn) {
 	// enable tax_amount field if Actual
 })
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 6836d56..d4ae66e 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -210,6 +210,10 @@
 		)
 	)
 
+	alternative_map = {
+		x.get("original_item") : x.get("alternative_item") for x in frappe.flags.get("args", {}).get("mapping", [])
+	}
+
 	def set_missing_values(source, target):
 		if customer:
 			target.customer = customer.name
@@ -233,6 +237,29 @@
 			target.blanket_order = obj.blanket_order
 			target.blanket_order_rate = obj.blanket_order_rate
 
+	def can_map_row(item) ->  bool:
+		"""
+		Row mapping from Quotation to Sales order:
+		1. Simple row: Map if adequate qty
+		2. Has Alternative Item: Map if no alternative was selected against original item and #1
+		3. Is Alternative Item: Map if alternative was selected against original item and #1
+		"""
+		has_qty = item.qty > 0
+
+		has_alternative = item.item_code in alternative_map
+		is_alternative = item.is_alternative
+
+		if not alternative_map or not (is_alternative or has_alternative):
+			# No alternative items in doc or current row is a simple item (without alternatives)
+			return has_qty
+
+		if is_alternative:
+			is_selected = alternative_map.get(item.alternative_to) == item.item_code
+		else:
+			is_selected = alternative_map.get(item.item_code) is None
+		return is_selected and has_qty
+
+
 	doclist = get_mapped_doc(
 		"Quotation",
 		source_name,
@@ -242,7 +269,7 @@
 				"doctype": "Sales Order Item",
 				"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
 				"postprocess": update_item,
-				"condition": lambda doc: doc.qty > 0,
+				"condition": can_map_row,
 			},
 			"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
 			"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},