diff --git a/cypress/integration/grid.js b/cypress/integration/grid.js
index 607db6746e..60f8601482 100644
--- a/cypress/integration/grid.js
+++ b/cypress/integration/grid.js
@@ -111,4 +111,76 @@ context("Grid", () => {
cy.get("@table-form").find(".grid-footer-toolbar").click();
});
});
+
+ it("shows edit button only when child table allow_bulk_edit is enabled", () => {
+ cy.visit("/desk/contact/Test Contact");
+ cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
+
+ cy.window()
+ .its("cur_frm")
+ .then((frm) => {
+ const grid = frm.get_field("phone_nos").grid;
+ grid.meta.allow_bulk_edit = false;
+ grid.refresh_edit_rows_button();
+ });
+
+ cy.get("@table").find('.grid-row[data-idx="1"] .grid-row-check').click({ force: true });
+ cy.get("@table").find(".grid-edit-rows").should("have.class", "hidden");
+
+ cy.window()
+ .its("cur_frm")
+ .then((frm) => {
+ const grid = frm.get_field("phone_nos").grid;
+ grid.meta.allow_bulk_edit = true;
+ grid.refresh_edit_rows_button();
+ });
+
+ cy.get("@table").find(".grid-edit-rows").should("not.have.class", "hidden");
+ });
+
+ it("bulk edit updates only selected child rows", () => {
+ const updated_phone = `99999${Date.now().toString().slice(-5)}`;
+
+ cy.visit("/desk/contact/Test Contact");
+ cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
+
+ cy.window()
+ .its("cur_frm")
+ .then((frm) => {
+ const grid = frm.get_field("phone_nos").grid;
+ grid.meta.allow_bulk_edit = true;
+ grid.refresh_edit_rows_button();
+
+ expect(frm.doc.phone_nos.length).to.be.greaterThan(1);
+ const phone_df = grid.docfields.find((df) => df.fieldname === "phone");
+ expect(phone_df).to.exist;
+ cy.wrap(phone_df.label).as("phoneFieldLabel");
+ cy.wrap(frm.doc.phone_nos[1].phone || "").as("secondRowPhoneBefore");
+ });
+
+ cy.get("@table").find('.grid-row[data-idx="1"] .grid-row-check').click({ force: true });
+ cy.get("@table").find(".grid-edit-rows").click({ force: true });
+
+ cy.window()
+ .its("cur_dialog")
+ .then((dialog) => {
+ cy.get("@phoneFieldLabel").then((phoneFieldLabel) => {
+ return dialog
+ .set_value("field", phoneFieldLabel)
+ .then(() => dialog.set_value("value", updated_phone))
+ .then(() => {
+ dialog.get_primary_btn().click();
+ });
+ });
+ });
+
+ cy.window().its("cur_frm.doc.phone_nos.0.phone").should("eq", updated_phone);
+ cy.window()
+ .its("cur_frm")
+ .then((frm) => {
+ cy.get("@secondRowPhoneBefore").then((secondRowPhoneBefore) => {
+ expect(frm.doc.phone_nos[1].phone || "").to.equal(secondRowPhoneBefore);
+ });
+ });
+ });
});
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index f77190d18d..c237d840c0 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_bulk_edit": 1,
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2013-02-18 13:36:19",
@@ -34,6 +35,7 @@
"quick_entry",
"grid_page_length",
"rows_threshold_for_grid_search",
+ "allow_bulk_edit",
"cb01",
"track_changes",
"track_seen",
@@ -715,6 +717,14 @@
"fieldname": "recipient_account_field",
"fieldtype": "Data",
"label": "Recipient Account Field"
+ },
+ {
+ "default": "1",
+ "depends_on": "istable",
+ "description": "Enable bulk update of this field across child table rows.",
+ "fieldname": "allow_bulk_edit",
+ "fieldtype": "Check",
+ "label": "Allow Bulk Edit"
}
],
"grid_page_length": 50,
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 39f33d19e4..cf9543d1f1 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -101,6 +101,7 @@ class DocType(Document):
actions: DF.Table[DocTypeAction]
allow_auto_repeat: DF.Check
+ allow_bulk_edit: DF.Check
allow_copy: DF.Check
allow_events_in_timeline: DF.Check
allow_guest_to_view: DF.Check
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index 5cb6e91398..f04563a1cb 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -24,6 +24,7 @@
"track_views",
"allow_auto_repeat",
"allow_import",
+ "allow_bulk_edit",
"queue_in_background",
"naming_section",
"naming_rule",
@@ -222,6 +223,14 @@
"fieldtype": "Check",
"label": "Allow Import (via Data Import Tool)"
},
+ {
+ "default": "1",
+ "depends_on": "istable",
+ "description": "Enable bulk edit for child table fields in Form view.",
+ "fieldname": "allow_bulk_edit",
+ "fieldtype": "Check",
+ "label": "Allow Bulk Edit"
+ },
{
"depends_on": "email_append_to",
"fieldname": "subject_field",
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index e2646dc8c4..78625eb99c 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -42,6 +42,7 @@ class CustomizeForm(Document):
actions: DF.Table[DocTypeAction]
allow_auto_repeat: DF.Check
+ allow_bulk_edit: DF.Check
allow_copy: DF.Check
allow_import: DF.Check
autoname: DF.Data | None
@@ -744,6 +745,7 @@ doctype_properties = {
"track_views": "Check",
"allow_auto_repeat": "Check",
"allow_import": "Check",
+ "allow_bulk_edit": "Check",
"show_name_in_global_search": "Check",
"show_preview_popup": "Check",
"default_email_template": "Data",
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index ad7b2fa8c8..694c97799b 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -87,6 +87,10 @@ export default class Grid {
data-action="delete_rows">
${__("Delete")}
+