Repository: frappe/frappe

Issue:
migration_hash in {doctype}.json files 
Migrations using hashes were added via #14246. The column `migration_hash` was added as a hidden field in the doctype DocType. It doesn't make sense to export it in JSON as the calculated hash is supposed to be that of the whole JSON file, which changes if you insert new data (calculated hash) inside the same file.

Noticed this issue in the regeneration of JSON files via #14558 


Discussion:

Resolving patch:
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -87,10 +87,6 @@ def validate(self):
 		if self.default_print_format and not self.custom:
 			frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
 
-		if frappe.conf.get('developer_mode'):
-			self.owner = 'Administrator'
-			self.modified_by = 'Administrator'
-
 	def validate_field_name_conflicts(self):
 		"""Check if field names dont conflict with controller properties and methods"""
 		core_doctypes = [
@@ -177,7 +173,6 @@ def check_developer_mode(self):
 		if self.is_virtual and self.custom:
 			frappe.throw(_("Not allowed to create custom Virtual DocType."), CannotCreateStandardDoctypeError)
 
-
 		if frappe.conf.get('developer_mode'):
 			self.owner = 'Administrator'
 			self.modified_by = 'Administrator'
@@ -315,9 +310,7 @@ def on_update(self):
 		if allow_doctype_export:
 			self.export_doc()
 			self.make_controller_template()
-
-			if self.has_web_view:
-				self.set_base_class_for_controller()
+			self.set_base_class_for_controller()
 
 		# update index
 		if not self.custom:
@@ -355,23 +348,49 @@ def sync_global_search(self):
 				now=now, doctype=self.name)
 
 	def set_base_class_for_controller(self):
-		'''Updates the controller class to subclass from `WebsiteGenertor`,
-		if it is a subclass of `Document`'''
-		controller_path = frappe.get_module_path(frappe.scrub(self.module),
-			'doctype', frappe.scrub(self.name), frappe.scrub(self.name) + '.py')
+		"""If DocType.has_web_view has been changed, updates the controller class and import
+		from `WebsiteGenertor` to `Document` or viceversa"""
 
-		with open(controller_path, 'r') as f:
-			code = f.read()
+		if not self.has_value_changed("has_web_view"):
+			return
+
+		despaced_name = self.name.replace(" ", "_")
+		scrubbed_name = frappe.scrub(self.name)
+		scrubbed_module = frappe.scrub(self.module)
+		controller_path = frappe.get_module_path(
+			scrubbed_module, "doctype", scrubbed_name, f"{scrubbed_name}.py"
+		)
+
+		document_cls_tag = f"class {despaced_name}(Document)"
+		document_import_tag = "from frappe.model.document import Document"
+		website_generator_cls_tag = f"class {despaced_name}(WebsiteGenerator)"
+		website_generator_import_tag = "from frappe.website.generators.website_generator import WebsiteGenerator"
 
-		class_string = '\nclass {0}(Document)'.format(self.name.replace(' ', ''))
-		if '\nfrom frappe.model.document import Document' in code and class_string in code:
-			code = code.replace('from frappe.model.document import Document',
-				'from frappe.website.website_generator import WebsiteGenerator')
-			code = code.replace('class {0}(Document)'.format(self.name.replace(' ', '')),
-				'class {0}(WebsiteGenerator)'.format(self.name.replace(' ', '')))
+		with open(controller_path) as f:
+			code = f.read()
+		updated_code = code
+
+		is_website_generator_class = all([
+			website_generator_cls_tag in code,
+			website_generator_import_tag in code
+		])
+
+		if self.has_web_view and not is_website_generator_class:
+			updated_code = updated_code.replace(
+				document_import_tag, website_generator_import_tag
+			).replace(
+				document_cls_tag, website_generator_cls_tag
+			)
+		elif not self.has_web_view and is_website_generator_class:
+			updated_code = updated_code.replace(
+				website_generator_import_tag, document_import_tag
+			).replace(
+				website_generator_cls_tag, document_cls_tag
+			)
 
-		with open(controller_path, 'w') as f:
-			f.write(code)
+		if updated_code != code:
+			with open(controller_path, "w") as f:
+				f.write(updated_code)
 
 	def run_module_method(self, method):
 		from frappe.modules import load_doctype_module
diff --git a/frappe/modules/export_file.py b/frappe/modules/export_file.py
--- a/frappe/modules/export_file.py
+++ b/frappe/modules/export_file.py
@@ -24,7 +24,7 @@ def write_document_file(doc, record_module=None, create_init=True, folder_name=N
 	doc_export = doc.as_dict(no_nulls=True)
 	doc.run_method("before_export", doc_export)
 
-	strip_default_fields(doc, doc_export)
+	doc_export = strip_default_fields(doc, doc_export)
 	module = record_module or get_module_name(doc)
 
 	# create folder
@@ -42,12 +42,17 @@ def write_document_file(doc, record_module=None, create_init=True, folder_name=N
 
 def strip_default_fields(doc, doc_export):
 	# strip out default fields from children
+	if doc.doctype == "DocType" and doc.migration_hash:
+		del doc_export["migration_hash"]
+
 	for df in doc.meta.get_table_fields():
 		for d in doc_export.get(df.fieldname):
 			for fieldname in frappe.model.default_fields:
 				if fieldname in d:
 					del d[fieldname]
 
+	return doc_export
+
 def write_code_files(folder, fname, doc, doc_export):
 	'''Export code files and strip from values'''
 	if hasattr(doc, 'get_code_fields'):
@@ -59,8 +64,6 @@ def write_code_files(folder, fname, doc, doc_export):
 				# remove from exporting
 				del doc_export[key]
 
-
-
 def get_module_name(doc):
 	if doc.doctype  == 'Module Def':
 		module = doc.name


Test patch: