Multiple variant creation dialog (#11608)

* Multiple variant creation dialog

* variant dialog codacy fixes

* [multiple variants] show_alert, and other minors
diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py
index 821c81c..9817f0f 100644
--- a/erpnext/controllers/item_variant.py
+++ b/erpnext/controllers/item_variant.py
@@ -169,6 +169,74 @@
 
 	return variant
 
+@frappe.whitelist()
+def enqueue_multiple_variant_creation(item, args):
+	# There can be innumerable attribute combinations, enqueue
+	frappe.enqueue("erpnext.controllers.item_variant.create_multiple_variants",
+		item=item, args=args, now=frappe.flags.in_test);
+
+def create_multiple_variants(item, args):
+	if isinstance(args, basestring):
+		args = json.loads(args)
+
+	args_set = generate_keyed_value_combinations(args)
+
+	for attribute_values in args_set:
+		if not get_variant(item, args=attribute_values):
+			variant = create_variant(item, attribute_values)
+			variant.save()
+
+def generate_keyed_value_combinations(args):
+	"""
+	From this:
+
+		args = {"attr1": ["a", "b", "c"], "attr2": ["1", "2"], "attr3": ["A"]}
+
+	To this:
+
+		[
+			{u'attr1': u'a', u'attr2': u'1', u'attr3': u'A'},
+			{u'attr1': u'b', u'attr2': u'1', u'attr3': u'A'},
+			{u'attr1': u'c', u'attr2': u'1', u'attr3': u'A'},
+			{u'attr1': u'a', u'attr2': u'2', u'attr3': u'A'},
+			{u'attr1': u'b', u'attr2': u'2', u'attr3': u'A'},
+			{u'attr1': u'c', u'attr2': u'2', u'attr3': u'A'}
+		]
+
+	"""
+	# Return empty list if empty
+	if not args:
+		return []
+
+	# Turn `args` into a list of lists of key-value tuples:
+	# [
+	# 	[(u'attr2', u'1'), (u'attr2', u'2')],
+	# 	[(u'attr3', u'A')],
+	# 	[(u'attr1', u'a'), (u'attr1', u'b'), (u'attr1', u'c')]
+	# ]
+	key_value_lists = [[(key, val) for val in args[key]] for key in args.keys()]
+
+	# Store the first, but as objects
+	# [{u'attr2': u'1'}, {u'attr2': u'2'}]
+	results = key_value_lists.pop(0)
+	results = [{d[0]: d[1]} for d in results]
+
+	# Iterate the remaining
+	# Take the next list to fuse with existing results
+	for l in key_value_lists:
+		new_results = []
+		for res in results:
+			for key_val in l:
+				# create a new clone of object in result
+				obj = copy.deepcopy(res)
+				# to be used with every incoming new value
+				obj[key_val[0]] = key_val[1]
+				# and pushed into new_results
+				new_results.append(obj)
+		results = new_results
+
+	return results
+
 def copy_attributes_to_variant(item, variant):
 	from frappe.model import no_value_fields
 
@@ -208,7 +276,7 @@
 			attributes_description = ""
 			for d in variant.attributes:
 				attributes_description += "<div>" + d.attribute + ": " + cstr(d.attribute_value) + "</div>"
-			
+
 			if attributes_description not in variant.description:
 					variant.description += attributes_description
 
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 656ee69..a71e1ea 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -57,9 +57,19 @@
 				frappe.set_route("List", "Item", {"variant_of": frm.doc.name});
 			}, __("View"));
 
-			frm.add_custom_button(__("Variant"), function() {
-				erpnext.item.make_variant(frm);
-			}, __("Make"));
+			if(frm.doc.variant_based_on==="Item Attribute") {
+				frm.add_custom_button(__("Single Variant"), function() {
+					erpnext.item.show_single_variant_dialog(frm);
+				}, __("Make"));
+				frm.add_custom_button(__("Multiple Variants"), function() {
+					erpnext.item.show_multiple_variants_dialog(frm);
+				}, __("Make"));
+			} else {
+				frm.add_custom_button(__("Variant"), function() {
+					erpnext.item.show_modal_for_manufacturers(frm);
+				}, __("Make"));
+			}
+
 			frm.page.set_inner_btn_group_as_primary(__("Make"));
 		}
 		if (frm.doc.variant_of) {
@@ -263,14 +273,6 @@
 		}
 	},
 
-	make_variant: function(frm) {
-		if(frm.doc.variant_based_on==="Item Attribute") {
-			erpnext.item.show_modal_for_item_attribute_selection(frm);
-		} else {
-			erpnext.item.show_modal_for_manufacturers(frm);
-		}
-	},
-
 	show_modal_for_manufacturers: function(frm) {
 		var dialog = new frappe.ui.Dialog({
 			fields: [
@@ -301,7 +303,146 @@
 		dialog.show();
 	},
 
-	show_modal_for_item_attribute_selection: function(frm) {
+	show_multiple_variants_dialog: function(frm) {
+		var me = this;
+
+		if(me.multiple_variant_dialog) {
+			me.multiple_variant_dialog.show();
+			return;
+		}
+
+		let promises = [];
+		let attr_val_fields = {};
+
+		function make_fields_from_attribute_values(attr_dict) {
+			let fields = [];
+			Object.keys(attr_dict).forEach((name, i) => {
+				if(i % 3 === 0){
+					fields.push({fieldtype: 'Section Break'});
+				}
+				fields.push({fieldtype: 'Column Break', label: name});
+				attr_dict[name].forEach(value => {
+					fields.push({
+						fieldtype: 'Check',
+						label: value,
+						fieldname: value,
+						default: 0,
+						onchange: function() {
+							let selected_attributes = get_selected_attributes();
+							let lengths = [];
+							Object.keys(selected_attributes).map(key => {
+								lengths.push(selected_attributes[key].length);
+							});
+							if(lengths.includes(0)) {
+								me.multiple_variant_dialog.get_primary_btn().html(__("Make Variants"));
+								me.multiple_variant_dialog.disable_primary_action();
+							} else {
+								let no_of_combinations = lengths.reduce((a, b) => a * b, 1);
+								me.multiple_variant_dialog.get_primary_btn()
+									.html(__(
+										`Make ${no_of_combinations} Variant
+										${no_of_combinations === 1 ? '' : 's'}`
+									));
+								me.multiple_variant_dialog.enable_primary_action();
+							}
+						}
+					});
+				});
+			});
+			return fields;
+		}
+
+		function make_and_show_dialog(fields) {
+			me.multiple_variant_dialog = new frappe.ui.Dialog({
+				title: __("Select Attribute Values"),
+				fields: [
+					{
+						fieldtype: "HTML",
+						fieldname: "help",
+						options: `<label class="control-label">
+							${__("Select at least one value from each of the attributes.")}
+						</label>`,
+					}
+				].concat(fields)
+			});
+
+			me.multiple_variant_dialog.set_primary_action(__("Make Variants"), () => {
+				let selected_attributes = get_selected_attributes();
+
+				me.multiple_variant_dialog.hide();
+				frappe.call({
+					method:"erpnext.controllers.item_variant.enqueue_multiple_variant_creation",
+					args: {
+						"item": frm.doc.name,
+						"args": selected_attributes
+					},
+					callback: function() {
+						frappe.show_alert({
+							message: __("Variant creation has been queued."),
+							indicator: 'orange'
+						});
+					}
+				});
+			});
+
+			$($(me.multiple_variant_dialog.$wrapper.find('.form-column'))
+				.find('.frappe-control')).css('margin-bottom', '0px');
+
+			me.multiple_variant_dialog.disable_primary_action();
+			me.multiple_variant_dialog.clear();
+			me.multiple_variant_dialog.show();
+		}
+
+		function get_selected_attributes() {
+			let selected_attributes = {};
+			me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => {
+				if(i===0) return;
+				let attribute_name = $(col).find('label').html();
+				selected_attributes[attribute_name] = [];
+				let checked_opts = $(col).find('.checkbox input');
+				checked_opts.each((i, opt) => {
+					if($(opt).is(':checked')) {
+						selected_attributes[attribute_name].push($(opt).attr('data-fieldname'));
+					}
+				});
+			});
+
+			return selected_attributes;
+		}
+
+		let attribute_names = frm.doc.attributes.map(d => d.attribute);
+
+		attribute_names.forEach(function(attribute) {
+			let p = new Promise(resolve => {
+				frappe.call({
+					method:"frappe.client.get_list",
+					args:{
+						doctype:"Item Attribute Value",
+						filters: [
+							["parent","=", attribute]
+						],
+						fields: ["attribute_value"]
+					}
+				}).then((r) => {
+					if(r.message) {
+						attr_val_fields[attribute] = r.message.map(function(d) { return d.attribute_value; });
+						resolve();
+					}
+				});
+			});
+
+			promises.push(p);
+
+		}, this);
+
+		Promise.all(promises).then(() => {
+			let fields = make_fields_from_attribute_values(attr_val_fields);
+			make_and_show_dialog(fields);
+		})
+
+	},
+
+	show_single_variant_dialog: function(frm) {
 		var fields = []
 
 		for(var i=0;i< frm.doc.attributes.length;i++){