Create variants from Quick Entry dialog

Hello!

we are currently setting up ERPNext for our company.

I have noticed that it is unfortunately not possible in Version 15 to create a new variant item from a template item using the quick entry form.

This used to be possible in version 14.

As most of our products are quite complex variant products, it would be a great help, if we could configure and create new variants through the quick entry when creating a new quotation or sales order.

Is there any chance this feature can be implemented?

Thanks for your help and best regards from Germany!

Timmy

So, it seems like there have been some changes in the quick entry dialogs and also the way that variants are created between v14 and v15. I have now created a client script, which can simply be added to the Quotation and Sales Order doctypes and adds back the functionality of creating variants through the quick entry dialog.

Here is the client script:

frappe.provide("frappe.ui.form");

frappe.ui.form.ItemQuickEntryForm = class ItemQuickEntryForm extends frappe.ui.form.QuickEntryForm {
	constructor(doctype, after_insert) {
		super(doctype, after_insert);
	}

    render_dialog() {
        this.mandatory = this.get_variant_fields()
            .concat(this.get_mandatory_fields())
            .concat(this.get_attributes_fields());
        this.check_naming_series_based_on();
        super.render_dialog();
        this.init_post_render_dialog_operations();
        this.preset_fields_for_template();
        this.dialog.$wrapper
            .find(".edit-full")
            .text(__("Edit in full page for more options like assets, serial nos, batches etc."));
    }

    get_mandatory_fields() {
        return [
            { fieldname: 'item_code', fieldtype: 'Data', label: __('Item Code'), reqd: 1 },
            { fieldname: 'item_name', fieldtype: 'Data', label: __('Item Name'), reqd: 1 },
            {
                fieldname: 'item_group',
                fieldtype: 'Link',
                label: __('Item Group'),
                options: 'Item Group',
                reqd: 1
            },
            {
                fieldname: 'stock_uom',
                fieldtype: 'Link',
                label: __('Stock UOM'),
                options: 'UOM',
                reqd: 1
            }
        ];
    }

    check_naming_series_based_on() {
        if (frappe.defaults.get_default("item_naming_by") === "Naming Series") {
            this.mandatory = this.mandatory.filter((d) => d.fieldname !== "item_code");
        }
    }

	init_post_render_dialog_operations() {
		this.dialog.fields_dict.attribute_html.$wrapper.append(frappe.render_template("item_quick_entry"));
		this.init_for_create_variant_trigger();
		this.init_for_item_template_trigger();
		// explicitly hide manufacturing fields as hidden not working.
		this.toggle_manufacturer_fields();
		this.dialog.get_field("item_template").df.hidden = 1;
		this.dialog.get_field("item_template").refresh();
	}

    register_primary_action() {
        var me = this;
        this.dialog.set_primary_action(__("Save"), function () {
            if (me.dialog.working) return;
    
            var data = me.dialog.get_values();
    
            if (data) {
                me.dialog.working = true;
                var values = me.update_doc();
    
                if (me.dialog.get_value("create_variant")) {
                    // Collect attribute values
                    if (me.get_variant_doc()) {
                        // Create variant via server-side method
                        me.insert(values);
                    } else {
                        me.dialog.working = false;
                    }
                } else {
                    // Regular item insert
                    me.insert_item(values);
                }
            }
        });
    }

    insert_item(values) {
        let me = this;
        frappe.call({
            method: "frappe.client.save",
            args: {
                doc: me.dialog.doc,
            },
            callback: function (r) {
                me.dialog.hide();
                me.dialog.doc = r.message;
                if (frappe._from_link) {
                    frappe.ui.form.update_calling_link(me.dialog.doc);
                } else {
                    if (me.after_insert) {
                        me.after_insert(me.dialog.doc);
                    } else {
                        me.open_form_if_not_list();
                    }
                }
            },
            error: function () {
                me.open_doc();
            },
            always: function () {
                me.dialog.working = false;
            },
            freeze: true,
        });
    } 

    insert(variant_values) {
        let me = this;
        return new Promise((resolve) => {
            frappe.call({
                method: "erpnext.controllers.item_variant.create_variant",
                args: {
                    item: me.dialog.get_value("item_template"),
                    args: me.attribute_values,
                    variant_based_on: me.is_manufacturer ? "Manufacturer" : null,
                    variant: variant_values,
                },
                callback: function (r) {
                    if (r.message) {
                        // Now save the variant
                        frappe.call({
                            method: "frappe.client.insert",
                            args: {
                                doc: r.message
                            },
                            callback: function(res) {
                                me.dialog.hide();
                                me.dialog.doc = res.message;
                                if (frappe._from_link) {
                                    frappe.ui.form.update_calling_link(me.dialog.doc);
                                } else {
                                    if (me.after_insert) {
                                        me.after_insert(me.dialog.doc);
                                    } else {
                                        me.open_form_if_not_list();
                                    }
                                }
                                resolve(me.dialog.doc);
                            },
                            error: function() {
                                // If there is an error saving, open the doc in form
                                frappe.model.sync([r.message]);
                                frappe.set_route("Form", r.message.doctype, r.message.name);
                                resolve(null);
                            },
                            freeze: true,
                        });
                    } else {
                        frappe.msgprint(__('Variant could not be created.'));
                        resolve(null);
                        me.dialog.working = false;
                    }
                },
                error: function () {
                    me.open_doc();
                    resolve(null);
                    me.dialog.working = false;
                },
                freeze: true,
            });
        });
    }
    

	open_doc() {
		this.dialog.hide();
		this.update_doc();
		if (this.dialog.fields_dict.create_variant.$input.prop("checked")) {
			var template = this.dialog.fields_dict.item_template.input.value;
			if (template) frappe.set_route("Form", this.doctype, template);
		} else {
			frappe.set_route("Form", this.doctype, this.doc.name);
		}
	}

	get_variant_fields() {
		var variant_fields = [
			{
				fieldname: "create_variant",
				fieldtype: "Check",
				label: __("Create Variant"),
			},
			{
				fieldname: "item_template",
				label: __("Item Template"),
				reqd: 0,
				fieldtype: "Link",
				options: "Item",
				get_query: function () {
					return {
						filters: {
							has_variants: 1,
						},
					};
				},
			},
		];

		return variant_fields;
	}

	get_manufacturing_fields() {
		this.manufacturer_fields = [
			{
				fieldtype: "Link",
				options: "Manufacturer",
				label: "Manufacturer",
				fieldname: "manufacturer",
				hidden: 1,
				reqd: 0,
			},
			{
				fieldtype: "Data",
				label: "Manufacturer Part Number",
				fieldname: "manufacturer_part_no",
				hidden: 1,
				reqd: 0,
			},
		];
		return this.manufacturer_fields;
	}

	get_attributes_fields() {
		var attribute_fields = [
			{
				fieldname: "attribute_html",
				fieldtype: "HTML",
			},
		];

		attribute_fields = attribute_fields.concat(this.get_manufacturing_fields());
		return attribute_fields;
	}

	init_for_create_variant_trigger() {
		var me = this;

		this.dialog.fields_dict.create_variant.$input.on("click", function () {
			me.preset_fields_for_template();
			me.init_post_template_trigger_operations(false, [], true);
		});
	}

    preset_fields_for_template() {
        var for_variant = this.dialog.get_value("create_variant");
    
        // setup template field, seen and mandatory if variant
        let template_field = this.dialog.get_field("item_template");
        template_field.df.reqd = for_variant;
        template_field.set_value("");
        template_field.df.hidden = !for_variant;
        template_field.refresh();
    
        // hide properties for variant
        ["item_code", "item_name", "item_group", "stock_uom"].forEach((d) => {
            let f = this.dialog.get_field(d);
            if (f) {
                f.df.hidden = for_variant;
                f.refresh();
            }
        });
    
        this.dialog.get_field("attribute_html").toggle(false);
    
        // non mandatory for variants
        ["item_code", "stock_uom", "item_group"].forEach((d) => {
            let f = this.dialog.get_field(d);
            if (f) {
                f.df.reqd = !for_variant;
                f.refresh();
            }
        });
    }

	init_for_item_template_trigger() {
		var me = this;

		me.dialog.fields_dict["item_template"].df.onchange = () => {
			var template = me.dialog.fields_dict.item_template.input.value;
			me.template_doc = null;
			if (template) {
				frappe.call({
					method: "frappe.client.get",
					args: {
						doctype: "Item",
						name: template,
					},
					callback: function (r) {
						me.template_doc = r.message;
						me.is_manufacturer = false;

						if (me.template_doc.variant_based_on === "Manufacturer") {
							me.init_post_template_trigger_operations(true, [], true);
						} else {
							me.init_post_template_trigger_operations(
								false,
								me.template_doc.attributes,
								false
							);
							me.render_attributes(me.template_doc.attributes);
						}
					},
				});
			} else {
				me.dialog.get_field("attribute_html").toggle(false);
				me.init_post_template_trigger_operations(false, [], true);
			}
		};
	}

	init_post_template_trigger_operations(is_manufacturer, attributes, attributes_flag) {
		this.attributes = attributes;
		this.attribute_values = {};
		this.attributes_count = attributes.length;

		this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes").empty();
		this.is_manufacturer = is_manufacturer;
		this.toggle_manufacturer_fields();
		this.dialog.fields_dict.attribute_html.$wrapper
			.find(".attributes")
			.toggleClass("hide-control", attributes_flag);
		this.dialog.fields_dict.attribute_html.$wrapper
			.find(".attributes-header")
			.toggleClass("hide-control", attributes_flag);
	}

	toggle_manufacturer_fields() {
		var me = this;
		$.each(this.manufacturer_fields, function (i, dialog_field) {
			me.dialog.get_field(dialog_field.fieldname).df.hidden = !me.is_manufacturer;
			me.dialog.get_field(dialog_field.fieldname).df.reqd =
				dialog_field.fieldname == "manufacturer" ? me.is_manufacturer : false;
			me.dialog.get_field(dialog_field.fieldname).refresh();
		});
	}

	initiate_render_attributes() {
		this.dialog.fields_dict.attribute_html.$wrapper.find(".attributes").empty();
		this.render_attributes(this.attributes);
	}

	render_attributes(attributes) {
		var me = this;

		this.dialog.get_field("attribute_html").toggle(true);

		$.each(attributes, function (index, row) {
			var desc = "";
			var fieldtype = "Data";
			if (row.numeric_values) {
				fieldtype = "Float";
				desc =
					"Min Value: " +
					row.from_range +
					" , Max Value: " +
					row.to_range +
					", in Increments of: " +
					row.increment;
			}

			me.init_make_control(fieldtype, row);
			me[row.attribute].set_value(me.attribute_values[row.attribute] || "");
			me[row.attribute].$wrapper.toggleClass(
				"has-error",
				me.attribute_values[row.attribute] ? false : true
			);

			// Set Label explicitly as make_control is not displaying label
			$(me[row.attribute].label_area).text(__(row.attribute));

			if (desc) {
				$(
					repl(`<p class="help-box small text-muted hidden-xs">%(desc)s</p>`, {
						desc: desc,
					})
				).insertAfter(me[row.attribute].input_area);
			}

			if (!row.numeric_values) {
				me.init_awesomplete_for_attribute(row);
			} else {
				me[row.attribute].$input.on("change", function () {
					me.attribute_values[row.attribute] = $(this).val();
					$(this)
						.closest(".frappe-control")
						.toggleClass("has-error", $(this).val() ? false : true);
				});
			}
		});
	}

	init_make_control(fieldtype, row) {
		this[row.attribute] = frappe.ui.form.make_control({
			df: {
				fieldtype: fieldtype,
				label: row.attribute,
				fieldname: row.attribute,
				options: row.options || "",
			},
			parent: $(this.dialog.fields_dict.attribute_html.wrapper).find(".attributes"),
			only_input: false,
		});
		this[row.attribute].make_input();
	}

	init_awesomplete_for_attribute(row) {
		var me = this;

		this[row.attribute].input.awesomplete = new Awesomplete(this[row.attribute].input, {
			minChars: 0,
			maxItems: 99,
			autoFirst: true,
			list: [],
		});

		this[row.attribute].$input
			.on("input", function (e) {
				frappe.call({
					method: "frappe.client.get_list",
					args: {
						doctype: "Item Attribute Value",
						filters: [
							["parent", "=", $(e.target).attr("data-fieldname")],
							["attribute_value", "like", e.target.value + "%"],
						],
						fields: ["attribute_value"],
						parent: "Item Attribute",
					},
					callback: function (r) {
						if (r.message) {
							e.target.awesomplete.list = r.message.map(function (d) {
								return d.attribute_value;
							});
						}
					},
				});
			})
			.on("focus", function (e) {
				$(e.target).val("").trigger("input");
			})
			.on("awesomplete-close", function (e) {
				me.attribute_values[$(e.target).attr("data-fieldname")] = e.target.value;
				$(e.target)
					.closest(".frappe-control")
					.toggleClass("has-error", e.target.value ? false : true);
			});
	}

    get_variant_doc() {
        var me = this;
        var attribute = this.validate_mandatory_attributes();
    
        if (Object.keys(attribute).length) {
            // Set attribute_values to be used in the insert method
            me.attribute_values = attribute;
            return true;
        } else {
            // Handle missing attributes
            frappe.msgprint(__('Please enter attribute values.'));
            return false;
        }
    }
    

    validate_mandatory_attributes() {
        var me = this;
        var attribute = {};
        var mandatory = [];
    
        $.each(this.attributes, function (index, attr) {
            var value = me[attr.attribute].get_value();
            if (value) {
                attribute[attr.attribute] = attr.numeric_values ? flt(value) : value;
            } else {
                mandatory.push(attr.attribute);
            }
        });
    
        if (mandatory.length > 0) {
            frappe.msgprint(__('Please set values for attributes: {0}', [mandatory.join(', ')]));
            return null;
        }
    
        if (this.is_manufacturer) {
            $.each(this.manufacturer_fields, function (index, field) {
                attribute[field.fieldname] = me.dialog.get_value(field.fieldname);
            });
        }
    
        me.attribute_values = attribute;
        return attribute;
    }
    
};

(It is not extensively tested and I must give significant credit to the incredible ChatGPT / o1-preview model :sweat_smile: )

Hope it is helpful to someone