Repository: frappe/frappe

Issue:
Allow all users to save customized reports
### User story
If a user can see a report (type: Report Builder), he can add or remove columns and filters to customize his view. Consequently he should also be able to save this current view for future use.

### Situation
At the moment, only users with role "Report Manager" can save customized reports. A report manager can create, edit, delete all custom reports. This is way too permissive for a normal user.

### Proposed solution
A user can always customize the reports he can see, for himself. So everybody should have the permission to create, edit, delete his _own reports_ as long as this only means narrowing down an existing report.


Discussion:
https://github.com/frappe/frappe/pull/15494 is a good start.

Further recommendations:
- Save button should be visible only if User is Report's owner or Report Manager.
- A button to delete a report will be visible in Report View based on following conditions:
   - Report is of type Report Builder
   - User is owner / report manager
   - It is not a standard report

I think that the restriction to narrow down the existing report is not really necessary as permissions are already being checked when rendering the report using `frappe.get_list`.


Resolving patch:
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -262,22 +262,66 @@ def compress(data, args=None):
 	}
 
 @frappe.whitelist()
-def save_report():
-	"""save report"""
-
-	data = frappe.local.form_dict
-	if frappe.db.exists('Report', data['name']):
-		d = frappe.get_doc('Report', data['name'])
+def save_report(name, doctype, report_settings):
+	"""Save reports of type Report Builder from Report View"""
+
+	if frappe.db.exists('Report', name):
+		report = frappe.get_doc('Report', name)
+		if report.is_standard == "Yes":
+			frappe.throw(_("Standard Reports cannot be edited"))
+
+		if report.report_type != "Report Builder":
+			frappe.throw(_("Only reports of type Report Builder can be edited"))
+
+		if (
+			report.owner != frappe.session.user
+			and not frappe.has_permission("Report", "write")
+		):
+			frappe.throw(
+				_("Insufficient Permissions for editing Report"),
+				frappe.PermissionError
+			)
 	else:
-		d = frappe.new_doc('Report')
-		d.report_name = data['name']
-		d.ref_doctype = data['doctype']
-
-	d.report_type = "Report Builder"
-	d.json = data['json']
-	frappe.get_doc(d).save()
-	frappe.msgprint(_("{0} is saved").format(d.name), alert=True)
-	return d.name
+		report = frappe.new_doc('Report')
+		report.report_name = name
+		report.ref_doctype = doctype
+
+	report.report_type = "Report Builder"
+	report.json = report_settings
+	report.save(ignore_permissions=True)
+	frappe.msgprint(
+		_("Report {0} saved").format(frappe.bold(report.name)),
+		indicator="green",
+		alert=True,
+	)
+	return report.name
+
+@frappe.whitelist()
+def delete_report(name):
+	"""Delete reports of type Report Builder from Report View"""
+
+	report = frappe.get_doc("Report", name)
+	if report.is_standard == "Yes":
+		frappe.throw(_("Standard Reports cannot be deleted"))
+
+	if report.report_type != "Report Builder":
+		frappe.throw(_("Only reports of type Report Builder can be deleted"))
+
+	if (
+		report.owner != frappe.session.user
+		and not frappe.has_permission("Report", "delete")
+	):
+		frappe.throw(
+			_("Insufficient Permissions for deleting Report"),
+			frappe.PermissionError
+		)
+
+	report.delete(ignore_permissions=True)
+	frappe.msgprint(
+		_("Report {0} deleted").format(frappe.bold(report.name)),
+		indicator="green",
+		alert=True,
+	)
 
 @frappe.whitelist()
 @frappe.read_only()


Test patch:
diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py
--- a/frappe/core/doctype/report/test_report.py
+++ b/frappe/core/doctype/report/test_report.py
@@ -4,7 +4,9 @@
 import frappe, json, os
 import unittest
 from frappe.desk.query_report import run, save_report
+from frappe.desk.reportview import delete_report, save_report as _save_report
 from frappe.custom.doctype.customize_form.customize_form import reset_customization
+from frappe.core.doctype.user_permission.test_user_permission import create_user
 
 test_records = frappe.get_test_records('Report')
 test_dependencies = ['User']
@@ -30,6 +32,60 @@ def test_query_report(self):
 		self.assertEqual(columns[1].get('label'), 'Module')
 		self.assertTrue('User' in [d.get('name') for d in data])
 
+	def test_save_or_delete_report(self):
+		'''Test for validations when editing / deleting report of type Report Builder'''
+
+		try:
+			report = frappe.get_doc({
+				'doctype': 'Report',
+				'ref_doctype': 'User',
+				'report_name': 'Test Delete Report',
+				'report_type': 'Report Builder',
+				'is_standard': 'No',
+			}).insert()
+
+			# Check for PermissionError
+			create_user("test_report_owner@example.com", "Website Manager")
+			frappe.set_user("test_report_owner@example.com")
+			self.assertRaises(frappe.PermissionError, delete_report, report.name)
+
+			# Check for Report Type
+			frappe.set_user("Administrator")
+			report.db_set("report_type", "Custom Report")
+			self.assertRaisesRegex(
+				frappe.ValidationError,
+				"Only reports of type Report Builder can be deleted",
+				delete_report,
+				report.name
+			)
+
+			# Check if creating and deleting works with proper validations
+			frappe.set_user("test@example.com")
+			report_name = _save_report(
+				'Dummy Report',
+				'User',
+				json.dumps([{
+					'fieldname': 'email',
+					'fieldtype': 'Data',
+					'label': 'Email',
+					'insert_after_index': 0,
+					'link_field': 'name',
+					'doctype': 'User',
+					'options': 'Email',
+					'width': 100,
+					'id':'email',
+					'name': 'Email'
+				}])
+			)
+
+			doc = frappe.get_doc("Report", report_name)
+			delete_report(doc.name)
+
+		finally:
+			frappe.set_user("Administrator")
+			frappe.db.rollback()
+
+
 	def test_custom_report(self):
 		reset_customization('User')
 		custom_report_name = save_report(