fix(telephony): scope voice call settings to their user

Replace the `All` permission rules with `Desk User` and limit non-admin
users to their own Voice Call Settings through permission hooks. Sites
with customised permissions get the same rules through a patch.

(cherry picked from commit 7c88594ba8)

# Conflicts:
#	erpnext/hooks.py
#	erpnext/patches.txt
#	erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py
#	erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json
#	erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py
This commit is contained in:
Nabin Hait
2026-10-22 09:31:53 +00:00
committed by Mergify
parent e007159c21
commit b45caf3aeb
6 changed files with 167 additions and 5 deletions
+20
View File
@@ -288,6 +288,26 @@ sounds = [
has_upload_permission = {"Employee": "erpnext.setup.doctype.employee.employee.has_upload_permission"}
<<<<<<< HEAD
=======
permission_query_conditions = {
"Asset Activity": "erpnext.assets.doctype.asset_activity.asset_activity.get_permission_query_conditions",
"Item": "erpnext.stock.doctype.company_restriction.company_restriction.get_permission_query_conditions",
"Customer": "erpnext.stock.doctype.company_restriction.company_restriction.get_permission_query_conditions",
"Supplier": "erpnext.stock.doctype.company_restriction.company_restriction.get_permission_query_conditions",
"Voice Call Settings": "erpnext.telephony.doctype.voice_call_settings.voice_call_settings.get_permission_query_conditions",
"*": "erpnext.stock.doctype.company_restriction.company_restriction.get_inherited_permission_query_conditions",
}
has_permission = {
"Item": "erpnext.stock.doctype.company_restriction.company_restriction.has_permission",
"Customer": "erpnext.stock.doctype.company_restriction.company_restriction.has_permission",
"Supplier": "erpnext.stock.doctype.company_restriction.company_restriction.has_permission",
"Voice Call Settings": "erpnext.telephony.doctype.voice_call_settings.voice_call_settings.has_permission",
"*": "erpnext.stock.doctype.company_restriction.company_restriction.has_inherited_permission",
}
>>>>>>> 7c88594 (fix(telephony): scope voice call settings to their user)
has_website_permission = {
"Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission",
+12
View File
@@ -460,3 +460,15 @@ erpnext.patches.v16_0.remove_all_role_from_payment_terms_template
execute:from erpnext.setup.install import grant_address_read_to_accounts_manager; grant_address_read_to_accounts_manager()
erpnext.patches.v16_0.remove_invalid_fetch_from_italy_payment_schedule_swift_number
erpnext.patches.v16_0.set_user_on_timesheets
<<<<<<< HEAD
=======
erpnext.patches.v16_0.replace_legacy_print_formats_with_classic
erpnext.patches.v16_0.set_stock_value_in_stock_closing_balance
erpnext.patches.v16_0.reset_fifo_queue_in_stock_closing_balance
erpnext.patches.v16_0.remove_invalid_fields_from_copy_fields_to_variants
erpnext.patches.v16_0.fix_ifrs_cash_flow_template_formulas
erpnext.patches.v16_0.disable_add_to_transit_on_standard_stock_entry_types
erpnext.patches.v16_0.unhide_item_barcode_fields
execute:frappe.db.set_value("Report", "Negative Batch Report", "prepared_report", 1)
erpnext.patches.v16_0.scope_voice_call_settings_to_desk_users
>>>>>>> 7c88594 (fix(telephony): scope voice call settings to their user)
@@ -0,0 +1,24 @@
import frappe
from frappe.permissions import get_doctypes_with_custom_docperms
from frappe.utils import cint
from erpnext.patches.v16_0.mirror_release_perms_to_custom_docperm import mirror_rules
from erpnext.patches.v16_0.remove_all_role_from_payment_terms_template import remove_rule
from erpnext.setup.install import get_role_rules
DOCTYPE = "Voice Call Settings"
DESK_USER_PTYPES = ("read", "write", "create", "report", "export", "print", "email")
def execute():
"""Move customised sites from the `All` rule to the `Desk User` rule shipped in the DocType."""
if DOCTYPE not in get_doctypes_with_custom_docperms():
return
mirror_rules({DOCTYPE: {"Desk User": DESK_USER_PTYPES}})
removed = False
for rule in get_role_rules(DOCTYPE, "All"):
if not cint(rule.permlevel) and remove_rule(DOCTYPE, rule.name):
removed = True
if removed:
frappe.clear_cache(doctype=DOCTYPE)
@@ -1,9 +1,78 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
<<<<<<< HEAD
# import frappe
import unittest
class TestVoiceCallSettings(unittest.TestCase):
pass
=======
import frappe
import frappe.client
from erpnext.tests.permission_test_utils import as_user, make_fenced_user
from erpnext.tests.utils import ERPNextTestSuite
class TestVoiceCallSettings(ERPNextTestSuite):
def test_user_creates_own_settings(self):
user = make_user("voice-call-owner@example.com")
other_user = make_user("voice-call-other@example.com")
with self.set_user(user):
own = frappe.get_doc({"doctype": "Voice Call Settings", "user": user}).insert()
self.assertEqual((own.name, own.user), (user, user))
frappe.delete_doc("Voice Call Settings", own.name)
with self.set_user(user):
picked = frappe.get_doc({"doctype": "Voice Call Settings", "user": other_user}).insert()
self.assertEqual((picked.name, picked.user), (user, user))
def test_settings_are_limited_to_their_user(self):
owner = make_user("voice-call-owner@example.com")
settings = make_settings(owner, greeting_message="Owner greeting")
other_user = make_user("voice-call-other@example.com")
website_user = make_fenced_user("voice-call-website@example.com", [])
with as_user(website_user):
self.assertRaises(frappe.PermissionError, frappe.get_list, "Voice Call Settings")
with as_user(other_user):
self.assertNotIn(owner, frappe.get_list("Voice Call Settings", pluck="name"))
for outsider in (other_user, website_user):
with as_user(outsider):
self.assertRaises(frappe.PermissionError, frappe.client.get, "Voice Call Settings", owner)
self.assertRaises(
frappe.PermissionError,
frappe.client.set_value,
"Voice Call Settings",
owner,
"greeting_message",
"Changed",
)
self.assertRaises(frappe.PermissionError, frappe.delete_doc, "Voice Call Settings", owner)
with as_user(owner):
self.assertEqual(frappe.get_list("Voice Call Settings", pluck="name"), [owner])
frappe.client.set_value("Voice Call Settings", owner, "greeting_message", "Owner changed")
with as_user(make_fenced_user("voice-call-manager@example.com", ["System Manager"])):
self.assertIn(owner, frappe.get_list("Voice Call Settings", pluck="name"))
self.assertEqual(
frappe.client.get("Voice Call Settings", owner)["greeting_message"], "Owner changed"
)
self.assertTrue(frappe.db.exists("Voice Call Settings", settings.name))
def make_user(email: str) -> str:
return make_fenced_user(email, ["Sales User"])
def make_settings(user: str, **values) -> "frappe.model.document.Document":
if not frappe.db.exists("Voice Call Settings", user):
frappe.get_doc({"doctype": "Voice Call Settings", "user": user}).insert(ignore_permissions=True)
frappe.db.set_value("Voice Call Settings", user, values)
return frappe.get_doc("Voice Call Settings", user)
>>>>>>> 7c88594 (fix(telephony): scope voice call settings to their user)
@@ -53,7 +53,11 @@
],
"index_web_pages_for_search": 1,
"links": [],
<<<<<<< HEAD
"modified": "2020-12-14 18:49:34.600194",
=======
"modified": "2026-10-15 15:16:45.554169",
>>>>>>> 7c88594 (fix(telephony): scope voice call settings to their user)
"modified_by": "Administrator",
"module": "Telephony",
"name": "Voice Call Settings",
@@ -61,14 +65,12 @@
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"role": "Desk User",
"write": 1
},
{
@@ -114,8 +116,7 @@
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1
"role": "Desk User"
}
],
"sort_field": "modified",
@@ -5,6 +5,8 @@
# import frappe
from frappe.model.document import Document
ADMIN_ROLE = "System Manager"
class VoiceCallSettings(Document):
# begin: auto-generated types
@@ -22,4 +24,38 @@ class VoiceCallSettings(Document):
user: DF.Link
# end: auto-generated types
<<<<<<< HEAD
pass
=======
def before_insert(self):
self.set_user_for_restricted_creator()
def set_user_for_restricted_creator(self):
"""Users who may not pick the user create their own settings."""
if self.flags.ignore_permissions or self.has_permlevel_access_to("user", permission_type="write"):
return
self.user = frappe.session.user
self.flags.ignore_permlevel_for_fields = ["user"]
def has_permission(doc: VoiceCallSettings, ptype: str | None = None, user: str | None = None) -> bool:
"""Allow non-admin users to access only their own Voice Call Settings.
Create is not scoped here because `before_insert` assigns the record to the creator."""
user = user or frappe.session.user
return ptype == "create" or is_admin(user) or doc.user == user
def get_permission_query_conditions(user: str | None = None) -> str | None:
"""Restrict list views and queries to the user's own record for non-admin users."""
user = user or frappe.session.user
if is_admin(user):
return None
return f"`tabVoice Call Settings`.`user` = {frappe.db.escape(user)}"
def is_admin(user: str) -> bool:
return ADMIN_ROLE in frappe.get_roles(user)
>>>>>>> 7c88594 (fix(telephony): scope voice call settings to their user)