From 321751ab5a58d78441b8e8a14c87e99293f8953e Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 2 Jan 2025 23:13:29 +0530 Subject: [PATCH] fix: apply permlevel restrictions on datafields --- crm/api/doc.py | 64 -------------- .../crm_fields_layout/crm_fields_layout.json | 11 ++- .../crm_fields_layout/crm_fields_layout.py | 85 ++++++++++++++++++- .../doctype/crm_industry/crm_industry.json | 11 ++- crm/fcrm/doctype/crm_lead/crm_lead.json | 11 ++- .../crm_lead_source/crm_lead_source.json | 11 ++- .../crm_lead_status/crm_lead_status.json | 11 ++- frontend/src/components/Controls/Grid.vue | 2 + .../Controls/GridRowFieldsModal.vue | 20 ++++- .../src/components/Controls/GridRowModal.vue | 9 +- frontend/src/pages/Contact.vue | 6 +- frontend/src/pages/Deal.vue | 6 +- frontend/src/pages/Lead.vue | 6 +- frontend/src/pages/MobileContact.vue | 6 +- frontend/src/pages/MobileDeal.vue | 6 +- frontend/src/pages/MobileLead.vue | 6 +- frontend/src/pages/MobileOrganization.vue | 6 +- frontend/src/pages/Organization.vue | 6 +- 18 files changed, 184 insertions(+), 99 deletions(-) diff --git a/crm/api/doc.py b/crm/api/doc.py index 5f1c9cf2..11ddcf5b 100644 --- a/crm/api/doc.py +++ b/crm/api/doc.py @@ -565,70 +565,6 @@ def get_fields_meta(doctype, restricted_fieldtypes=None, as_array=False): return fields_meta -@frappe.whitelist() -def get_sidebar_fields(doctype, name): - if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}): - return [] - layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}).layout - - if not layout: - return [] - - layout = json.loads(layout) - - not_allowed_fieldtypes = [ - "Tab Break", - "Section Break", - "Column Break", - ] - - fields = frappe.get_meta(doctype).fields - fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes] - - doc = frappe.get_cached_doc(doctype, name) - has_high_permlevel_fields = any(df.permlevel > 0 for df in fields) - if has_high_permlevel_fields: - has_read_access_to_permlevels = doc.get_permlevel_access("read") - has_write_access_to_permlevels = doc.get_permlevel_access("write") - - for section in layout: - section["name"] = section.get("name") or section.get("label") - for column in section.get("columns") if section.get("columns") else []: - for field in column.get("fields") if column.get("fields") else []: - field_obj = next((f for f in fields if f.fieldname == field), None) - if field_obj: - if field_obj.permlevel > 0: - field_has_write_access = field_obj.permlevel in has_write_access_to_permlevels - field_has_read_access = field_obj.permlevel in has_read_access_to_permlevels - if not field_has_write_access and field_has_read_access: - field_obj.read_only = 1 - if not field_has_read_access and not field_has_write_access: - field_obj.hidden = 1 - column["fields"][column.get("fields").index(field)] = get_field_obj(field_obj) - - fields_meta = {} - for field in fields: - fields_meta[field.fieldname] = field - - return layout - - -def get_field_obj(field): - field = field.as_dict() - field["placeholder"] = field.get("placeholder") or "Add " + field.label + "..." - - if field.fieldtype == "Link": - field["placeholder"] = field.get("placeholder") or "Select " + field.label + "..." - elif field.fieldtype == "Select" and field.options: - field["placeholder"] = field.get("placeholder") or "Select " + field.label + "..." - field["options"] = [{"label": option, "value": option} for option in field.options.split("\n")] - - if field.read_only: - field["tooltip"] = "This field is read only and cannot be edited." - - return field - - def get_assigned_users(doctype, name, default_assigned_to=None): assigned_users = frappe.get_all( "ToDo", diff --git a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json index 1f246aab..43019c5c 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.json @@ -46,7 +46,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-01-01 20:42:29.862890", + "modified": "2025-01-02 22:12:51.663011", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Fields Layout", @@ -64,6 +64,15 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "sort_field": "creation", diff --git a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py index 395aef60..271a5da9 100644 --- a/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py +++ b/crm/fcrm/doctype/crm_fields_layout/crm_fields_layout.py @@ -14,7 +14,7 @@ class CRMFieldsLayout(Document): @frappe.whitelist() -def get_fields_layout(doctype: str, type: str): +def get_fields_layout(doctype: str, type: str, parent_doctype: str | None = None): tabs = [] layout = None @@ -52,11 +52,94 @@ def get_fields_layout(doctype: str, type: str): field = next((f for f in fields if f.fieldname == field), None) if field: field = field.as_dict() + handle_perm_level_restrictions(field, doctype, parent_doctype) column["fields"][column.get("fields").index(field["fieldname"])] = field return tabs or [] +@frappe.whitelist() +def get_sidepanel_sections(doctype): + if not frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}): + return [] + layout = frappe.get_doc("CRM Fields Layout", {"dt": doctype, "type": "Side Panel"}).layout + + if not layout: + return [] + + layout = json.loads(layout) + + not_allowed_fieldtypes = [ + "Tab Break", + "Section Break", + "Column Break", + ] + + fields = frappe.get_meta(doctype).fields + fields = [field for field in fields if field.fieldtype not in not_allowed_fieldtypes] + + for section in layout: + section["name"] = section.get("name") or section.get("label") + for column in section.get("columns") if section.get("columns") else []: + for field in column.get("fields") if column.get("fields") else []: + field_obj = next((f for f in fields if f.fieldname == field), None) + if field_obj: + field_obj = field_obj.as_dict() + handle_perm_level_restrictions(field_obj, doctype) + column["fields"][column.get("fields").index(field)] = get_field_obj(field_obj) + + fields_meta = {} + for field in fields: + fields_meta[field.fieldname] = field + + return layout + + +def handle_perm_level_restrictions(field, doctype, parent_doctype=None): + if field.permlevel == 0: + return + field_has_write_access = field.permlevel in get_permlevel_access("write", doctype, parent_doctype) + field_has_read_access = field.permlevel in get_permlevel_access("read", doctype, parent_doctype) + + if not field_has_write_access and field_has_read_access: + field.read_only = 1 + if not field_has_read_access and not field_has_write_access: + field.hidden = 1 + + +def get_permlevel_access(permission_type="write", doctype=None, parent_doctype=None): + allowed_permlevels = [] + roles = frappe.get_roles() + + meta = frappe.get_meta(doctype) + + if meta.istable and parent_doctype: + meta = frappe.get_meta(parent_doctype) + elif meta.istable and not parent_doctype: + return [1, 0] + + for perm in meta.permissions: + if perm.role in roles and perm.get(permission_type) and perm.permlevel not in allowed_permlevels: + allowed_permlevels.append(perm.permlevel) + + return allowed_permlevels + + +def get_field_obj(field): + field["placeholder"] = field.get("placeholder") or "Add " + field.label + "..." + + if field.fieldtype == "Link": + field["placeholder"] = field.get("placeholder") or "Select " + field.label + "..." + elif field.fieldtype == "Select" and field.options: + field["placeholder"] = field.get("placeholder") or "Select " + field.label + "..." + field["options"] = [{"label": option, "value": option} for option in field.options.split("\n")] + + if field.read_only: + field["tooltip"] = "This field is read only and cannot be edited." + + return field + + @frappe.whitelist() def save_fields_layout(doctype: str, type: str, layout: str): if frappe.db.exists("CRM Fields Layout", {"dt": doctype, "type": type}): diff --git a/crm/fcrm/doctype/crm_industry/crm_industry.json b/crm/fcrm/doctype/crm_industry/crm_industry.json index d3d15a71..5128d8be 100644 --- a/crm/fcrm/doctype/crm_industry/crm_industry.json +++ b/crm/fcrm/doctype/crm_industry/crm_industry.json @@ -21,7 +21,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-19 21:57:02.025918", + "modified": "2025-01-02 22:14:28.686821", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Industry", @@ -51,6 +51,15 @@ "role": "Sales Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "quick_entry": 1, diff --git a/crm/fcrm/doctype/crm_lead/crm_lead.json b/crm/fcrm/doctype/crm_lead/crm_lead.json index 786c03a5..e39af407 100644 --- a/crm/fcrm/doctype/crm_lead/crm_lead.json +++ b/crm/fcrm/doctype/crm_lead/crm_lead.json @@ -290,7 +290,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2024-09-17 18:36:57.289897", + "modified": "2025-01-02 22:14:01.991054", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Lead", @@ -320,6 +320,15 @@ "role": "Sales Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "sender_field": "email", diff --git a/crm/fcrm/doctype/crm_lead_source/crm_lead_source.json b/crm/fcrm/doctype/crm_lead_source/crm_lead_source.json index 23aea6f6..416ccd7e 100644 --- a/crm/fcrm/doctype/crm_lead_source/crm_lead_source.json +++ b/crm/fcrm/doctype/crm_lead_source/crm_lead_source.json @@ -29,7 +29,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-19 21:56:04.702254", + "modified": "2025-01-02 22:13:30.498404", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Lead Source", @@ -59,6 +59,15 @@ "role": "Sales Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "quick_entry": 1, diff --git a/crm/fcrm/doctype/crm_lead_status/crm_lead_status.json b/crm/fcrm/doctype/crm_lead_status/crm_lead_status.json index 587f6b61..21ca8b61 100644 --- a/crm/fcrm/doctype/crm_lead_status/crm_lead_status.json +++ b/crm/fcrm/doctype/crm_lead_status/crm_lead_status.json @@ -37,7 +37,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-19 21:56:16.872924", + "modified": "2025-01-02 22:13:43.038656", "modified_by": "Administrator", "module": "FCRM", "name": "CRM Lead Status", @@ -67,6 +67,15 @@ "role": "Sales Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "sort_field": "modified", diff --git a/frontend/src/components/Controls/Grid.vue b/frontend/src/components/Controls/Grid.vue index e866bab1..519f7a1b 100644 --- a/frontend/src/components/Controls/Grid.vue +++ b/frontend/src/components/Controls/Grid.vue @@ -170,6 +170,7 @@ :index="index" :data="row" :doctype="doctype" + :parentDoctype="parentDoctype" /> @@ -198,6 +199,7 @@ v-if="showGridRowFieldsModal" v-model="showGridRowFieldsModal" :doctype="doctype" + :parentDoctype="parentDoctype" /> - + @@ -55,6 +61,10 @@ const props = defineProps({ type: String, default: 'CRM Lead', }, + parentDoctype: { + type: String, + default: '', + }, }) const emit = defineEmits(['reload']) @@ -66,12 +76,16 @@ const dirty = ref(false) const preview = ref(false) function getParams() { - return { doctype: _doctype.value, type: 'Grid Row' } + return { + doctype: _doctype.value, + type: 'Grid Row', + parent_doctype: props.parentDoctype, + } } const tabs = createResource({ url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', - cache: ['GridRowFieldsModal', _doctype.value], + cache: ['GridRowFieldsModal', _doctype.value, props.parentDoctype], params: getParams(), onSuccess(data) { tabs.originalData = JSON.parse(JSON.stringify(data)) diff --git a/frontend/src/components/Controls/GridRowModal.vue b/frontend/src/components/Controls/GridRowModal.vue index 93351b4e..36fe0846 100644 --- a/frontend/src/components/Controls/GridRowModal.vue +++ b/frontend/src/components/Controls/GridRowModal.vue @@ -41,6 +41,7 @@ const props = defineProps({ index: Number, data: Object, doctype: String, + parentDoctype: String, }) const { isManager } = usersStore() @@ -50,8 +51,12 @@ const showGridRowFieldsModal = defineModel('showGridRowFieldsModal') const tabs = createResource({ url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_fields_layout', - cache: ['Grid Row', props.doctype], - params: { doctype: props.doctype, type: 'Grid Row' }, + cache: ['Grid Row', props.doctype, props.parentDoctype], + params: { + doctype: props.doctype, + type: 'Grid Row', + parent_doctype: props.parentDoctype, + }, auto: true, }) diff --git a/frontend/src/pages/Contact.vue b/frontend/src/pages/Contact.vue index b2035d48..c6d08a76 100644 --- a/frontend/src/pages/Contact.vue +++ b/frontend/src/pages/Contact.vue @@ -336,9 +336,9 @@ const rows = computed(() => { }) const sections = createResource({ - url: 'crm.api.doc.get_sidebar_fields', - cache: ['sidePanelSections', props.contactId], - params: { doctype: 'Contact', name: props.contactId }, + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections', + cache: ['sidePanelSections', 'Contact'], + params: { doctype: 'Contact' }, auto: true, transform: (data) => getParsedSections(data), }) diff --git a/frontend/src/pages/Deal.vue b/frontend/src/pages/Deal.vue index 6c5ac9b7..90c9cb93 100644 --- a/frontend/src/pages/Deal.vue +++ b/frontend/src/pages/Deal.vue @@ -530,9 +530,9 @@ const tabs = computed(() => { const { tabIndex } = useActiveTabManager(tabs, 'lastDealTab') const sections = createResource({ - url: 'crm.api.doc.get_sidebar_fields', - cache: ['sidePanelSections', props.dealId], - params: { doctype: 'CRM Deal', name: props.dealId }, + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections', + cache: ['sidePanelSections', 'CRM Deal'], + params: { doctype: 'CRM Deal' }, auto: true, transform: (data) => getParsedSections(data), }) diff --git a/frontend/src/pages/Lead.vue b/frontend/src/pages/Lead.vue index b396fc09..c59547b2 100644 --- a/frontend/src/pages/Lead.vue +++ b/frontend/src/pages/Lead.vue @@ -531,9 +531,9 @@ function validateFile(file) { } const sections = createResource({ - url: 'crm.api.doc.get_sidebar_fields', - cache: ['sidePanelSections', props.leadId], - params: { doctype: 'CRM Lead', name: props.leadId }, + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections', + cache: ['sidePanelSections', 'CRM Lead'], + params: { doctype: 'CRM Lead' }, auto: true, }) diff --git a/frontend/src/pages/MobileContact.vue b/frontend/src/pages/MobileContact.vue index c97e6b5f..452d611b 100644 --- a/frontend/src/pages/MobileContact.vue +++ b/frontend/src/pages/MobileContact.vue @@ -348,9 +348,9 @@ const rows = computed(() => { }) const fieldsLayout = createResource({ - url: 'crm.api.doc.get_sidebar_fields', - cache: ['fieldsLayout', props.contactId], - params: { doctype: 'Contact', name: props.contactId }, + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections', + cache: ['sidePanelSections', 'Contact'], + params: { doctype: 'Contact' }, auto: true, transform: (data) => getParsedFields(data), }) diff --git a/frontend/src/pages/MobileDeal.vue b/frontend/src/pages/MobileDeal.vue index 58efd5b9..72de7654 100644 --- a/frontend/src/pages/MobileDeal.vue +++ b/frontend/src/pages/MobileDeal.vue @@ -501,9 +501,9 @@ const tabs = computed(() => { const { tabIndex } = useActiveTabManager(tabs, 'lastDealTab') const fieldsLayout = createResource({ - url: 'crm.api.doc.get_sidebar_fields', - cache: ['fieldsLayout', props.dealId], - params: { doctype: 'CRM Deal', name: props.dealId }, + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections', + cache: ['sidePanelSections', 'CRM Deal'], + params: { doctype: 'CRM Deal' }, auto: true, transform: (data) => getParsedFields(data), }) diff --git a/frontend/src/pages/MobileLead.vue b/frontend/src/pages/MobileLead.vue index 56d39589..26452022 100644 --- a/frontend/src/pages/MobileLead.vue +++ b/frontend/src/pages/MobileLead.vue @@ -421,9 +421,9 @@ watch(tabs, (value) => { }) const fieldsLayout = createResource({ - url: 'crm.api.doc.get_sidebar_fields', - cache: ['fieldsLayout', props.leadId], - params: { doctype: 'CRM Lead', name: props.leadId }, + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections', + cache: ['sidePanelSections', 'CRM Lead'], + params: { doctype: 'CRM Lead' }, auto: true, }) diff --git a/frontend/src/pages/MobileOrganization.vue b/frontend/src/pages/MobileOrganization.vue index 8b74f968..2694b28d 100644 --- a/frontend/src/pages/MobileOrganization.vue +++ b/frontend/src/pages/MobileOrganization.vue @@ -327,9 +327,9 @@ const _organization = ref({}) const _address = ref({}) const fieldsLayout = createResource({ - url: 'crm.api.doc.get_sidebar_fields', - cache: ['fieldsLayout', props.organizationId], - params: { doctype: 'CRM Organization', name: props.organizationId }, + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections', + cache: ['sidePanelSections', 'CRM Organization'], + params: { doctype: 'CRM Organization' }, auto: true, transform: (data) => getParsedFields(data), }) diff --git a/frontend/src/pages/Organization.vue b/frontend/src/pages/Organization.vue index 7e6954d2..c1832046 100644 --- a/frontend/src/pages/Organization.vue +++ b/frontend/src/pages/Organization.vue @@ -336,9 +336,9 @@ const _organization = ref({}) const _address = ref({}) const sections = createResource({ - url: 'crm.api.doc.get_sidebar_fields', - cache: ['sidePanelSections', props.organizationId], - params: { doctype: 'CRM Organization', name: props.organizationId }, + url: 'crm.fcrm.doctype.crm_fields_layout.crm_fields_layout.get_sidepanel_sections', + cache: ['sidePanelSections', 'CRM Organization'], + params: { doctype: 'CRM Organization' }, auto: true, transform: (data) => getParsedSections(data), })