Quick Entry Forms
Build custom Quick Entry dialogs that create several linked documents from a single form.
On this page
Custom Quick Entry dialogs provide efficient data entry that can create multiple linked documents in a single form submission.
Basic Pattern
Extend frappe.ui.form.QuickEntryForm to add custom fields and behavior:
frappe.ui.form.MyQuickEntryForm = class MyQuickEntryForm extends (
frappe.ui.form.QuickEntryForm
) {
constructor(doctype, after_insert, init_callback, doc, force) {
super(doctype, after_insert, init_callback, doc, force);
this.skip_redirect_on_error = true; // Keep dialog open on failure
}
render_dialog() {
// Add custom fields before rendering
this.mandatory = this.mandatory.concat(this.get_variant_fields());
super.render_dialog();
}
get_variant_fields() {
return [
// Custom fields added to the quick entry dialog
];
}
};Registration
Wire the custom form to a doctype's list view via frappe.listview_settings:
frappe.listview_settings["Player"] = {
hide_name_column: true,
hide_name_filter: true,
quick_entry: true,
quick_entry_doc: frappe.ui.form.PlayerQuickEntryForm,
};File location: <doctype_name>_list.js alongside the DocType definition. Frappe automatically loads these for list views.
Adding Custom Fields
The get_variant_fields() method returns field definitions using the same schema as DocType fields:
get_variant_fields() {
return [
{
label: __("Create New Family"),
fieldname: "create_new_family",
fieldtype: "Check",
},
{
fieldtype: "Section Break",
label: __("Family Details"),
depends_on: "eval:doc.create_new_family",
},
{
label: __("Family Name"),
fieldname: "family_name",
fieldtype: "Data",
mandatory_depends_on: "eval:doc.create_new_family",
},
{
fieldtype: "Section Break",
label: __("Existing Family"),
depends_on: "eval:!doc.create_new_family",
},
{
label: __("Family"),
fieldname: "family",
fieldtype: "Link",
options: "Family",
mandatory_depends_on: "eval:!doc.create_new_family",
},
{
fieldtype: "Section Break",
label: __("Guardian Contact"),
depends_on: "eval:doc.create_new_family",
},
{
label: __("Email"),
fieldname: "email_address",
fieldtype: "Data",
options: "Email",
},
{
label: __("Phone"),
fieldname: "phone",
fieldtype: "Data",
options: "Phone",
},
];
}Key field properties:
depends_on— Conditional visibility usingeval:expressionsmandatory_depends_on— Conditional mandatory requirementoptions: "Email"/options: "Phone"— Built-in validation for Data fields
Multi-Document Creation
Override insert() to create linked documents before the main document:
async insert() {
if (this.dialog.doc.create_new_family) {
await this.create_family();
}
return super.insert();
}
async create_family() {
const doc = this.dialog.doc;
// 1. Create the Family
const family = await frappe.db.insert({
doctype: "Family",
family_name: doc.family_name,
});
// 2. Create a Contact for the guardian
const contact = await frappe.db.insert({
doctype: "Contact",
first_name: doc.first_name,
last_name: doc.last_name,
email_ids: [{ email_id: doc.email_address, is_primary: 1 }],
phone_nos: [{ phone: doc.phone, is_primary_mobile_no: 1 }],
});
// 3. Add guardian to the family
await frappe.db.insert({
doctype: "Family Guardian",
parent: family.name,
parenttype: "Family",
parentfield: "guardians",
guardian: contact.name,
});
// 4. Set the family on the main document
this.dialog.doc.family = family.name;
// 5. Clean up temporary fields
delete this.dialog.doc.family_name;
delete this.dialog.doc.email_address;
delete this.dialog.doc.phone;
delete this.dialog.doc.create_new_family;
}Alias Field Mapping
When the target doctype has readonly fields that would be hidden in dialogs, use alias fieldnames and remap them before insertion:
insert() {
const map_field_names = {
email_address: "email_id", // email_id is readonly on Contact
mobile_number: "mobile_no", // mobile_no is readonly on Contact
map_to_first_name: "first_name",
map_to_last_name: "last_name",
};
Object.entries(map_field_names).forEach(([alias, real]) => {
this.dialog.doc[real] = this.dialog.doc[alias];
delete this.dialog.doc[alias];
});
return super.insert();
}This lets users fill in email_address (which is visible and editable) instead of email_id (which Frappe hides because it's readonly).
Combining Address and Contact Entry
A common pattern merges two linked doctypes into one quick entry:
get_variant_fields() {
return [
// Contact fields
{ fieldtype: "Section Break", label: __("Contact Details") },
{ label: __("First Name"), fieldname: "map_to_first_name", fieldtype: "Data" },
{ label: __("Last Name"), fieldname: "map_to_last_name", fieldtype: "Data" },
{ label: __("Email"), fieldname: "email_address", fieldtype: "Data", options: "Email" },
{ label: __("Phone"), fieldname: "mobile_number", fieldtype: "Data", options: "Phone" },
// Address fields
{ fieldtype: "Section Break", label: __("Address Details") },
{ label: __("Address Line 1"), fieldname: "address_line1", fieldtype: "Data" },
{ label: __("City"), fieldname: "city", fieldtype: "Data" },
{ label: __("State"), fieldname: "state", fieldtype: "Data" },
{ label: __("Country"), fieldname: "country", fieldtype: "Link", options: "Country" },
{ label: __("ZIP Code"), fieldname: "pincode", fieldtype: "Data" },
];
}Key Design Decisions
| Decision | Approach |
|---|---|
| Keep dialog open on error | this.skip_redirect_on_error = true in constructor |
| Add fields before rendering | Modify this.mandatory in render_dialog() before super.render_dialog() |
| Conditional field visibility | Use depends_on: "eval:doc.field_name" |
| Create linked docs | Override insert(), create via frappe.db.insert(), then call super.insert() |
| Handle readonly target fields | Use alias fieldnames + remap in insert() |
| Clean up temp fields | delete this.dialog.doc.temp_field before super.insert() |
This article is in the public domain (CC0 1.0), code samples included. Use it however helps you.