diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 2d047d4c30..2bfd8d908f 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: __(
+ "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, + }, ], 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, }, }); }, diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index d7d3824fb0..35a3f1f708 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -56,21 +56,41 @@ 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) + + 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")) custom = { - "custom_fields": frappe.get_all("Custom Field", fields="*", filters={"dt": doctype}, order_by="name"), + "custom_fields": frappe.get_all( + "Custom Field", + fields="*", + filters=cf_filters, + order_by="name", + ), "property_setters": frappe.get_all( - "Property Setter", fields="*", filters={"doc_type": doctype}, order_by="name" + "Property Setter", + fields="*", + filters=ps_filters, + order_by="name", ), "custom_perms": [], "links": frappe.get_all("DocType Link", fields="*", filters={"parent": doctype}, order_by="name"), @@ -85,7 +105,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") 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" )