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++){