diff --git a/frappe/core/doctype/permission_inspector/permission_inspector.js b/frappe/core/doctype/permission_inspector/permission_inspector.js index 9c12080e94..a9f5eb921e 100644 --- a/frappe/core/doctype/permission_inspector/permission_inspector.js +++ b/frappe/core/doctype/permission_inspector/permission_inspector.js @@ -13,6 +13,7 @@ frappe.ui.form.on("Permission Inspector", { ref_doctype(frm) { frm.doc.docname = ""; // Usually doctype change invalidates docname call_debug(frm); + frm.trigger("add_custom_perm_types"); }, user: call_debug, permission_type: call_debug, @@ -21,4 +22,20 @@ frappe.ui.form.on("Permission Inspector", { frm.call("debug"); } }, + add_custom_perm_types(frm) { + const custom_perm_types = frm.doc.__onload.custom_perm_types + if (!custom_perm_types?.length) return + if (!frm.doc.ref_doctype) return + + const standard_options = frm.meta.fields.find(f => f.fieldname === "permission_type").options; + + const custom_options = ( + custom_perm_types + .filter(pt => pt.applicable_doctype != frm.doc.ref_doctype) + .map(pt => pt.label || pt.name) + .join("\n") + ); + + frm.set_df_property("permission_type", "options", `${standard_options}\n${custom_options}`); + } }); diff --git a/frappe/core/doctype/permission_inspector/permission_inspector.py b/frappe/core/doctype/permission_inspector/permission_inspector.py index 54af240f06..082d79a73f 100644 --- a/frappe/core/doctype/permission_inspector/permission_inspector.py +++ b/frappe/core/doctype/permission_inspector/permission_inspector.py @@ -37,6 +37,12 @@ class PermissionInspector(Document): user: DF.Link # end: auto-generated types + def onload(self): + self.set_onload("custom_perm_types", frappe.get_all( + "Permission Type", + fields=["name", "label", "applicable_for"], + )) + @frappe.whitelist() def debug(self): if not (self.ref_doctype and self.user): diff --git a/frappe/core/doctype/permission_type/permission_type.py b/frappe/core/doctype/permission_type/permission_type.py index ededa32681..8d905a97c4 100644 --- a/frappe/core/doctype/permission_type/permission_type.py +++ b/frappe/core/doctype/permission_type/permission_type.py @@ -1,11 +1,17 @@ # Copyright (c) 2025, Frappe Technologies and contributors # For license information, please see license.txt +from collections import defaultdict + import frappe from frappe import _ from frappe.model.document import Document from frappe.modules.export_file import delete_folder from frappe.modules.utils import get_doctype_module +from frappe.utils.caching import site_cache + +# doctypes where custom fields for permission types will be created +CUSTOM_FIELD_TARGET = ["Custom DocPerm", "DocPerm", "DocShare"] class PermissionType(Document): @@ -30,20 +36,17 @@ class PermissionType(Document): module = get_doctype_module(self.applicable_for) export_to_files(record_list=[["Permission Type", self.name]], record_module=module) - doctypes = ["Custom DocPerm", "DocPerm"] - for doctype in doctypes: - self.create_custom_docperm(doctype) + for doctype in CUSTOM_FIELD_TARGET: + self.create_custom_field(doctype) - def create_custom_docperm(self, doctype): + def create_custom_field(self, doctype): from frappe.custom.doctype.custom_field.custom_field import create_custom_field - if not frappe.db.exists( - doctype, - { - "fieldname": self.name, - "parent": self.applicable_for, - }, - ): + if not self.custom_field_exists(doctype): + depends_on = f"eval:doc.parent == '{self.applicable_for}'" + if doctype == "DocShare": + depends_on = f"eval:doc.share_doctype == '{self.applicable_for}'" + create_custom_field( doctype, { @@ -51,7 +54,7 @@ class PermissionType(Document): "label": self.name.replace("_", " ").title(), "fieldtype": "Check", "insert_after": "append", - "depends_on": f"eval:doc.parent == '{self.applicable_for}'", + "depends_on": depends_on, }, ) @@ -59,18 +62,34 @@ class PermissionType(Document): if not frappe.conf.developer_mode and not frappe.flags.in_migrate: frappe.throw(_("Deletion of this document is only permitted in developer mode.")) - for doctype in ["Custom DocPerm", "DocPerm"]: - self.delete_custom_docperm(doctype) + for doctype in CUSTOM_FIELD_TARGET: + self.delete_custom_field(doctype) module = get_doctype_module(self.applicable_for) delete_folder(module, "Permission Type", self.name) - def delete_custom_docperm(self, doctype): - if name := frappe.db.exists( + def delete_custom_field(self, doctype): + if name := self.custom_field_exists(doctype): + frappe.delete_doc("Custom Field", name) + + def custom_field_exists(self, doctype): + return frappe.db.exists( "Custom Field", { "fieldname": self.name, "dt": doctype, }, - ): - frappe.delete_doc("Custom Field", name) + ) + + +@site_cache +def get_custom_ptype_map(): + ptypes = frappe.get_all( + "Permission Type", + fields=["name", "label", "applicable_for"], + order_by="name", + ) + custom_ptype_map = defaultdict(list) + for pt in ptypes: + custom_ptype_map[pt["applicable_for"]].append(pt["label"] or pt["name"]) + return dict(custom_ptype_map) diff --git a/frappe/permissions.py b/frappe/permissions.py index 2ae9d25d13..d6a1272856 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -6,6 +6,7 @@ import functools import frappe import frappe.share from frappe import _, msgprint +from frappe.core.doctype.permission_type.permission_type import get_custom_ptype_map from frappe.query_builder import DocType from frappe.utils import cint, cstr @@ -166,7 +167,10 @@ def has_permission( ) def false_if_not_shared(): - if ptype not in ("read", "write", "share", "submit", "email", "print"): + std_rights = ["read", "write", "share", "submit", "email", "print"] + custom_rights = get_custom_ptype_map().get(doctype, []) + + if ptype not in std_rights + custom_rights: debug and _debug_log(f"Permission type {ptype} can not be shared") return False