diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index f50ceb1992..7b55b4bc6b 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -102,6 +102,20 @@ class CustomField(Document):
# delete property setter entries
frappe.db.delete("Property Setter", {"doc_type": self.dt, "field_name": self.fieldname})
+
+ # update doctype layouts
+ doctype_layouts = frappe.get_all(
+ "DocType Layout", filters={"document_type": self.dt}, pluck="name"
+ )
+
+ for layout in doctype_layouts:
+ layout_doc = frappe.get_doc("DocType Layout", layout)
+ for field in layout_doc.fields:
+ if field.fieldname == self.fieldname:
+ layout_doc.remove(field)
+ layout_doc.save()
+ break
+
frappe.clear_cache(doctype=self.dt)
def validate_insert_after(self, meta):
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.js b/frappe/custom/doctype/doctype_layout/doctype_layout.js
index f91f04f762..45c2de8447 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.js
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.js
@@ -2,31 +2,88 @@
// For license information, please see license.txt
frappe.ui.form.on("DocType Layout", {
- refresh: function (frm) {
- frm.trigger("document_type");
- frm.events.set_button(frm);
+ onload_post_render(frm) {
+ // disallow users from manually adding/deleting rows; this doctype should only
+ // be used for managing layout, and docfields and custom fields should be used
+ // to manage other field metadata (hidden, etc.)
+ frm.set_df_property("fields", "cannot_add_rows", true);
+ frm.set_df_property("fields", "cannot_delete_rows", true);
+ },
+
+ refresh(frm) {
+ frm.events.add_buttons(frm);
},
document_type(frm) {
- frm.set_fields_as_options("fields", frm.doc.document_type, null, [], "fieldname").then(
- () => {
- // child table empty? then show all fields as default
- if (frm.doc.document_type) {
- if (!(frm.doc.fields || []).length) {
- for (let f of frappe.get_doc("DocType", frm.doc.document_type).fields) {
- frm.add_child("fields", { fieldname: f.fieldname, label: f.label });
- }
- }
- }
- }
- );
+ if (frm.doc.document_type) {
+ frm.set_value("fields", []);
+ frm.events.sync_fields(frm, false);
+ }
},
- set_button(frm) {
+ add_buttons(frm) {
if (!frm.is_new()) {
frm.add_custom_button(__("Go to {0} List", [frm.doc.name]), () => {
window.open(`/app/${frappe.router.slug(frm.doc.name)}`);
});
+
+ frm.add_custom_button(__("Sync {0} Fields", [frm.doc.name]), async () => {
+ await frm.events.sync_fields(frm, true);
+ });
+ }
+ },
+
+ async sync_fields(frm, notify) {
+ frappe.dom.freeze("Fetching fields...");
+ const response = await frm.call({ doc: frm.doc, method: "sync_fields" });
+ frm.refresh_field("fields");
+ frappe.dom.unfreeze();
+
+ if (!response.message) {
+ frappe.msgprint(__("No changes to sync"));
+ return;
+ }
+
+ frm.dirty();
+ if (notify) {
+ const addedFields = response.message.added;
+ const removedFields = response.message.removed;
+
+ const getChangedMessage = (fields) => {
+ let changes = "";
+ for (const field of fields) {
+ if (field.label) {
+ changes += `
Row #${field.idx}: ${field.fieldname.bold()} (${
+ field.label
+ })`;
+ } else {
+ changes += `Row #${field.idx}: ${field.fieldname.bold()}`;
+ }
+ }
+ return changes;
+ };
+
+ let message = "";
+
+ if (addedFields.length) {
+ message += `The following fields have been added:
${getChangedMessage(
+ addedFields
+ )}
`;
+ }
+
+ if (removedFields.length) {
+ message += `The following fields have been removed:
${getChangedMessage(
+ removedFields
+ )}
`;
+ }
+
+ if (message) {
+ frappe.msgprint({
+ message: __(message),
+ indicator: "green",
+ title: __("Synced Fields"),
+ });
+ }
}
},
});
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.json b/frappe/custom/doctype/doctype_layout/doctype_layout.json
index e47c9e03e0..0b627f78ce 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.json
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.json
@@ -1,7 +1,7 @@
{
"actions": [],
"allow_rename": 1,
- "autoname": "Prompt",
+ "autoname": "prompt",
"creation": "2020-11-16 17:05:35.306846",
"doctype": "DocType",
"editable_grid": 1,
@@ -19,7 +19,8 @@
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
- "reqd": 1
+ "reqd": 1,
+ "set_only_once": 1
},
{
"fieldname": "fields",
@@ -42,10 +43,11 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-12-10 15:01:04.352184",
+ "modified": "2022-09-01 03:22:33.973058",
"modified_by": "Administrator",
"module": "Custom",
"name": "DocType Layout",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -68,5 +70,6 @@
"route": "doctype-layout",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py
index ea8e9acc99..778f2aa024 100644
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py
@@ -1,11 +1,70 @@
# Copyright (c) 2020, Frappe Technologies and contributors
# License: MIT. See LICENSE
+from typing import TYPE_CHECKING
+
+import frappe
from frappe.desk.utils import slug
from frappe.model.document import Document
+if TYPE_CHECKING:
+ from frappe.core.doctype.docfield.docfield import DocField
+
class DocTypeLayout(Document):
def validate(self):
if not self.route:
self.route = slug(self.name)
+
+ @frappe.whitelist()
+ def sync_fields(self):
+ layout_fieldnames = {field.fieldname for field in self.fields}
+ doctype_fields = frappe.get_meta(self.document_type).fields
+ doctype_fieldnames = {field.fieldname for field in doctype_fields}
+
+ added_fields = list(doctype_fieldnames - layout_fieldnames)
+ removed_fields = list(layout_fieldnames - doctype_fieldnames)
+
+ if not (added_fields or removed_fields):
+ return
+
+ added = self.add_fields(added_fields, doctype_fields)
+ removed = self.remove_fields(removed_fields)
+
+ for index, field in enumerate(self.fields):
+ field.idx = index + 1
+
+ return {"added": added, "removed": removed}
+
+ def add_fields(self, added_fields: list[str], doctype_fields: list["DocField"]) -> list[dict]:
+ added = []
+ for field in added_fields:
+ field_details = next((f for f in doctype_fields if f.fieldname == field), None)
+ if not field_details:
+ continue
+
+ # remove 'doctype' data from the DocField to allow adding it to the layout
+ row = self.append("fields", field_details.as_dict(no_default_fields=True))
+ if field_details.insert_after:
+ insert_after = next(
+ (f for f in self.fields if f.fieldname == field_details.insert_after),
+ None,
+ )
+
+ # initialize new row to just after the insert_after field
+ self.fields.insert(insert_after.idx, row)
+ self.fields.pop()
+
+ added.append({"idx": insert_after.idx + 1, "fieldname": row.fieldname, "label": row.label})
+ else:
+ added.append(row.as_dict())
+ return added
+
+ def remove_fields(self, removed_fields: list[str]) -> list[dict]:
+ removed = []
+ for field in removed_fields:
+ field_details = next((f for f in self.fields if f.fieldname == field), None)
+ if field_details:
+ self.remove(field_details)
+ removed.append(field_details.as_dict())
+ return removed