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 )
Hope it is helpful to someone