Repository: frappe/frappe

Issue:
DocType Layout Fields table does not populate
## Description of the issue
When selecting a Document Type in a DocType Layout document the Fields table does not populate. The document must be saved twice in order to populate the fields, because the first save resulting in an error message. Obviously this would lead people (like myself) to believe the DocType Layout feature is non-functional.

Here is an image demonstrating the problems:
![image](https://user-images.githubusercontent.com/16672299/159814715-b8642de3-97c7-4fee-b151-2ee18fef3ecd.png)

## Context information (for bug reports)

**Output of `bench version`**
```
ERPNext: v13.22.0 (version-13)
Frappe Framework: v13.22.0 (version-13)
```

## Steps to reproduce the issue

1. Add a new DocType Layout document
2. Enter a value in Document Type

### Observed result

- Fields table does not populate.

### Expected result

Something more like this
![image](https://user-images.githubusercontent.com/16672299/159814647-dccba2a6-61be-4796-8431-325c18e24abb.png)

## Additional information

From what I can tell, there are two issues at play:

1. The document_type field expects no rows to exist (but a blank row is auto-generated)
2. The method neglects to refresh the Fields table


Discussion:
raised a pr for this issue https://github.com/frappe/frappe/pull/16418

Resolving patch:
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -102,6 +102,20 @@ def on_trash(self):
 
 		# 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.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py
--- a/frappe/custom/doctype/doctype_layout/doctype_layout.py
+++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py
@@ -1,11 +1,77 @@
 # 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):
+		doctype_fields = frappe.get_meta(self.document_type).fields
+
+		if self.is_new():
+			added_fields = [field.fieldname for field in doctype_fields]
+			removed_fields = []
+		else:
+			doctype_fieldnames = {field.fieldname for field in doctype_fields}
+			layout_fieldnames = {field.fieldname for field in self.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))
+			row_data = row.as_dict()
+
+			if field_details.get("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
+				if insert_after:
+					self.fields.insert(insert_after.idx, row)
+					self.fields.pop()
+
+					row_data = {"idx": insert_after.idx + 1, "fieldname": row.fieldname, "label": row.label}
+
+			added.append(row_data)
+		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


Test patch: