From 39b523ea96940d62fbb942635531dd6005e8c07d Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Sat, 1 Nov 2025 21:41:49 +0000 Subject: [PATCH 1/6] feat: add check to apply module export filter in Export Customizations dialog --- frappe/custom/doctype/customize_form/customize_form.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 2d047d4c30..5d6b2ace2e 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -265,6 +265,15 @@ frappe.ui.form.on("Customize Form", { ), default: 0, }, + { + fieldtype: "Check", + fieldname: "apply_module_export_filter", + label: __("Apply Module Export Filter"), + description: __( + "Apply Module (for export) filter while exporting customizations." + ), + default: 0, + }, ], function (data) { frappe.call({ @@ -274,6 +283,7 @@ frappe.ui.form.on("Customize Form", { module: data.module, sync_on_migrate: data.sync_on_migrate, with_permissions: data.with_permissions, + apply_module_export_filter: data.apply_module_export_filter, }, }); }, From 4a538339a5a681c061351000b398de513bdf9194 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Sat, 1 Nov 2025 21:45:33 +0000 Subject: [PATCH 2/6] feat: add option to apply module export filter in export_customizations function --- frappe/modules/utils.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 30713a684f..eb62d994b1 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -53,21 +53,35 @@ def get_doc_module(module: str, doctype: str, name: str) -> "ModuleType": @frappe.whitelist() def export_customizations( - module: str, doctype: str, sync_on_migrate: bool = False, with_permissions: bool = False + module: str, + doctype: str, + sync_on_migrate: bool = False, + with_permissions: bool = False, + apply_module_export_filter: bool = False, ): """Export Custom Field and Property Setter for the current document to the app folder. This will be synced with bench migrate""" sync_on_migrate = cint(sync_on_migrate) with_permissions = cint(with_permissions) + apply_module_export_filter = cint(apply_module_export_filter) + module_export_filter = module if apply_module_export_filter else "" if not frappe.conf.developer_mode: frappe.throw(_("Only allowed to export customizations in developer mode")) custom = { - "custom_fields": frappe.get_all("Custom Field", fields="*", filters={"dt": doctype}, order_by="name"), + "custom_fields": frappe.get_all( + "Custom Field", + fields="*", + filters={"dt": doctype, "module": module_export_filter}, + order_by="name", + ), "property_setters": frappe.get_all( - "Property Setter", fields="*", filters={"doc_type": doctype}, order_by="name" + "Property Setter", + fields="*", + filters={"doc_type": doctype, "module": module_export_filter}, + order_by="name", ), "custom_perms": [], "links": frappe.get_all("DocType Link", fields="*", filters={"parent": doctype}, order_by="name"), @@ -82,7 +96,9 @@ def export_customizations( # also update the custom fields and property setters for all child tables for d in frappe.get_meta(doctype).get_table_fields(): - export_customizations(module, d.options, sync_on_migrate, with_permissions) + export_customizations( + module, d.options, sync_on_migrate, with_permissions, apply_module_export_filter + ) if custom["custom_fields"] or custom["property_setters"] or custom["custom_perms"]: folder_path = os.path.join(get_module_path(module), "custom") From 337a4bcfd21ae7c4b93c9d1ca060aa9a21ce44fe Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Tue, 9 Dec 2025 11:21:26 +0000 Subject: [PATCH 3/6] fix: apply module export filter only when checked --- frappe/modules/utils.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 8ad5127cba..83fd7b3457 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -67,7 +67,13 @@ def export_customizations( sync_on_migrate = cint(sync_on_migrate) with_permissions = cint(with_permissions) apply_module_export_filter = cint(apply_module_export_filter) - module_export_filter = module if apply_module_export_filter else "" + + cf_filters = {"dt": doctype} + ps_filters = {"doc_type": doctype} + + if apply_module_export_filter: + cf_filters["module"] = module + ps_filters["module"] = module if not frappe.conf.developer_mode: frappe.throw(_("Only allowed to export customizations in developer mode")) @@ -76,13 +82,13 @@ def export_customizations( "custom_fields": frappe.get_all( "Custom Field", fields="*", - filters={"dt": doctype, "module": module_export_filter}, + filters=cf_filters, order_by="name", ), "property_setters": frappe.get_all( "Property Setter", fields="*", - filters={"doc_type": doctype, "module": module_export_filter}, + filters=ps_filters, order_by="name", ), "custom_perms": [], From 78e75510cc15db6c3f604f81c9261f1ae96d5776 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Tue, 9 Dec 2025 11:47:25 +0000 Subject: [PATCH 4/6] refactor: enhance description for module export filter in Customize Form --- frappe/custom/doctype/customize_form/customize_form.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 5d6b2ace2e..ed8f412d16 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -270,7 +270,10 @@ frappe.ui.form.on("Customize Form", { fieldname: "apply_module_export_filter", label: __("Apply Module Export Filter"), description: __( - "Apply Module (for export) filter while exporting customizations." + "Export only customizations assigned to the selected module.
\ + Note: You must set the Module (for export) field on Custom Field and Property Setter records before applying this filter.\ +

Warning: Customizations from other modules will be excluded.

\ + " ), default: 0, }, From d07a90092ff27be51f408d410d94efb0eb0b8384 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Tue, 9 Dec 2025 12:30:18 +0000 Subject: [PATCH 5/6] test: add unit tests for exporting customizations with module filter --- frappe/tests/test_modules.py | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/frappe/tests/test_modules.py b/frappe/tests/test_modules.py index 22a8359528..f54bddfc7e 100644 --- a/frappe/tests/test_modules.py +++ b/frappe/tests/test_modules.py @@ -84,6 +84,53 @@ class TestUtils(IntegrationTestCase): self.assertTrue(file_path.endswith("/custom/custom/note.json")) self.assertTrue(os.path.exists(file_path)) + @unittest.skipUnless( + os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable" + ) + def test_export_customizations_with_module_filter(self): + # create two customizations, one matching the module, one under a different module + with note_customizations() as (custom_field, property_setter): + custom_field.db_set("module", "Custom") + property_setter.db_set("module", "Custom") + + # create module def called OtherModule + other_module = frappe.new_doc("Module Def") + + other_module.update({"module_name": "OtherModule", "app_name": "frappe"}) + other_module.save(ignore_permissions=True) + self.addCleanup(other_module.delete) + + # create a customization belonging to another module (should be excluded) + other_cf = create_custom_field( + "Note", + df={ + "fieldname": "other_mod_field", + "label": "Other Mod Field", + "fieldtype": "Data", + "module": "OtherModule", + }, + ) + + self.addCleanup(other_cf.delete) + + file_path = export_customizations( + module="Custom", + doctype="Note", + apply_module_export_filter=True, + ) + self.addCleanup(delete_file, path=file_path) + + self.assertTrue(os.path.exists(file_path)) + + with open(file_path) as f: + exported = frappe.parse_json(f.read()) + + exported_fields = {f["fieldname"] for f in exported.get("custom_fields", [])} + + self.assertIn("test_export_customizations_field", exported_fields) + + self.assertNotIn("other_mod_field", exported_fields) + @unittest.skipUnless( os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable" ) From e5a237fc9d99e5e0fa257e8afb577ba624b83931 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Tue, 9 Dec 2025 12:50:21 +0000 Subject: [PATCH 6/6] refactor: make description on single one line --- frappe/custom/doctype/customize_form/customize_form.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index ed8f412d16..2bfd8d908f 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -270,10 +270,7 @@ frappe.ui.form.on("Customize Form", { fieldname: "apply_module_export_filter", label: __("Apply Module Export Filter"), description: __( - "Export only customizations assigned to the selected module.
\ - Note: You must set the Module (for export) field on Custom Field and Property Setter records before applying this filter.\ -

Warning: Customizations from other modules will be excluded.

\ - " + "Export only customizations assigned to the selected module.
Note: You must set the Module (for export) field on Custom Field and Property Setter records before applying this filter.

Warning: Customizations from other modules will be excluded.

" ), default: 0, },