diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml index fe02351a5a..4875e6f5df 100644 --- a/.github/workflows/patch-mariadb-tests.yml +++ b/.github/workflows/patch-mariadb-tests.yml @@ -64,9 +64,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: | - 3.7 - 3.10 + python-version: "3.10" - name: Setup Node uses: actions/setup-node@v3 @@ -112,8 +110,8 @@ jobs: - name: Run Patch Tests run: | cd ~/frappe-bench/ - wget https://frappeframework.com/files/v10-frappe.sql.gz - bench --site test_site --force restore ~/frappe-bench/v10-frappe.sql.gz + wget https://frappeframework.com/files/v13-frappe.sql.gz + bench --site test_site --force restore ~/frappe-bench/v13-frappe.sql.gz source env/bin/activate cd apps/frappe/ @@ -121,7 +119,6 @@ jobs: function update_to_version() { version=$1 - py=$2 branch_name="version-$version-hotfix" echo "Updating to v$version" @@ -130,22 +127,22 @@ jobs: pgrep honcho | xargs kill rm -rf ~/frappe-bench/env - bench -v setup env --python $py - bench start &> ~/frappe-bench/bench_start.log & + bench -v setup env + bench start &>> ~/frappe-bench/bench_start.log & bench --site test_site migrate } - update_to_version 12 python3.7 - update_to_version 13 python3.7 - - update_to_version 14 python3.10 + update_to_version 14 echo "Updating to last commit" + pgrep honcho | xargs kill rm -rf ~/frappe-bench/env git checkout -q -f "$GITHUB_SHA" bench -v setup env + bench start &>> ~/frappe-bench/bench_start.log & bench --site test_site migrate + bench --site test_site execute frappe.tests.utils.check_orpahned_doctypes - name: Show bench output if: ${{ always() }} diff --git a/cypress/integration/control_currency.js b/cypress/integration/control_currency.js index 5e6db86036..1fb912d9ff 100644 --- a/cypress/integration/control_currency.js +++ b/cypress/integration/control_currency.js @@ -47,6 +47,17 @@ context("Control Currency", () => { df_options: { precision: 0 }, blur_expected: "10", }, + { + input: "10.000", + number_format: "#.###,##", + df_options: { precision: 0 }, + blur_expected: "10.000", + }, + { + input: "10.000", + number_format: "#.###,##", + blur_expected: "10.000,00", + }, { input: "10.101", df_options: { precision: "" }, @@ -61,6 +72,7 @@ context("Control Currency", () => { .then((frappe) => { frappe.boot.sysdefaults.currency = test_case.currency; frappe.boot.sysdefaults.currency_precision = test_case.default_precision ?? 2; + frappe.boot.sysdefaults.number_format = test_case.number_format ?? "#,###.##"; }); get_dialog_with_currency(test_case.df_options).as("dialog"); diff --git a/cypress/integration/control_float.js b/cypress/integration/control_float.js index 65aa21ed69..e7d6b398f1 100644 --- a/cypress/integration/control_float.js +++ b/cypress/integration/control_float.js @@ -83,6 +83,23 @@ context("Control Float", () => { }, ], }, + { + // '.' is the parseFloat's decimal separator + number_format: "#.###,##", + values: [ + { + input: "12.345", + blur_expected: "12.345,000", + focus_expected: "12345", + }, + { + // parseFloat would reduce 12,340 to 12,34 if this string was ever to be parsed + input: "12.340", + blur_expected: "12.340,000", + focus_expected: "12340", + }, + ], + }, ]; } }); diff --git a/cypress/integration/rounding.js b/cypress/integration/rounding.js index 1a9cfa685b..f778e009bb 100644 --- a/cypress/integration/rounding.js +++ b/cypress/integration/rounding.js @@ -49,7 +49,7 @@ context("Rounding behaviour", () => { let rounding_method = "Banker's Rounding"; expect(flt("0.5", 0, null, rounding_method)).eq(0); - expect(flt("0.3", null, rounding_method)).eq(0.3); + expect(flt("0.3", null, null, rounding_method)).eq(0.3); expect(flt("1.5", 0, null, rounding_method)).eq(2); diff --git a/frappe/__init__.py b/frappe/__init__.py index dd15329106..b676a6abf7 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -170,6 +170,8 @@ lang = local("lang") # This if block is never executed when running the code. It is only used for # telling static code analyzer where to find dynamically defined attributes. if TYPE_CHECKING: + from werkzeug.wrappers import Request + from frappe.database.mariadb.database import MariaDBDatabase from frappe.database.postgres.database import PostgresDatabase from frappe.model.document import Document @@ -179,6 +181,15 @@ if TYPE_CHECKING: db: MariaDBDatabase | PostgresDatabase qb: MariaDB | Postgres cache: RedisWrapper + response: _dict + conf: _dict + form_dict: _dict + flags: _dict + request: Request + session: _dict + user: str + flags: _dict + lang: str # end: static analysis hack diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 54d0e5fb7d..c1c7589564 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -18,8 +18,11 @@ from frappe.core.doctype.doctype.doctype import ( WrongOptionsDoctypeLinkError, validate_links_table_fieldnames, ) +from frappe.core.doctype.rq_job.test_rq_job import wait_for_completion from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.desk.form.load import getdoc +from frappe.model.delete_doc import delete_controllers +from frappe.model.sync import remove_orphan_doctypes from frappe.tests.utils import FrappeTestCase @@ -739,6 +742,21 @@ class TestDocType(FrappeTestCase): self.assertEqual(frappe.get_meta(doctype).get_field(field).default, "DELETETHIS") frappe.delete_doc("DocType", doctype) + @unittest.skipUnless( + os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable" + ) + @patch.dict(frappe.conf, {"developer_mode": 1}) + def test_delete_orphaned_doctypes(self): + doctype = new_doctype(custom=0).insert() + frappe.db.commit() + + delete_controllers(doctype.name, doctype.module) + job = frappe.enqueue(remove_orphan_doctypes) + wait_for_completion(job) + + frappe.db.rollback() + self.assertFalse(frappe.db.exists("DocType", doctype.name)) + def test_not_in_list_view_for_not_allowed_mandatory_field(self): doctype = new_doctype( fields=[ diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 697aa9a300..0443766de1 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -86,7 +86,7 @@ class Report(Document): if ( self.is_standard == "Yes" and not cint(getattr(frappe.local.conf, "developer_mode", 0)) - and not frappe.flags.in_patch + and not (frappe.flags.in_migrate or frappe.flags.in_patch) ): frappe.throw(_("You are not allowed to delete Standard Report")) delete_custom_role("report", self.name) diff --git a/frappe/core/doctype/rq_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py index a8e11269ee..fc191da233 100644 --- a/frappe/core/doctype/rq_job/test_rq_job.py +++ b/frappe/core/doctype/rq_job/test_rq_job.py @@ -15,16 +15,20 @@ from frappe.utils import cstr, execute_in_shell from frappe.utils.background_jobs import get_job_status, is_job_enqueued +@timeout(seconds=20) +def wait_for_completion(job: Job): + while True: + if not (job.is_queued or job.is_started): + break + time.sleep(0.2) + + class TestRQJob(FrappeTestCase): BG_JOB = "frappe.core.doctype.rq_job.test_rq_job.test_func" - @timeout(seconds=20) def check_status(self, job: Job, status, wait=True): - while wait: - if not (job.is_queued or job.is_started): - break - time.sleep(0.2) - + if wait: + wait_for_completion(job) self.assertEqual(frappe.get_doc("RQ Job", job.id).status, status) def test_serialization(self): diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index bc27106068..f06e583bee 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -44,22 +44,23 @@ frappe.PermissionEngine = class PermissionEngine { } setup_page() { - this.doctype_select = this.wrapper.page - .add_select( - __("Document Type"), - [{ value: "", label: __("Select Document Type") + "..." }].concat( - this.options.doctypes - ) - ) - .change(function () { - frappe.set_route("permission-manager", $(this).val()); - }); + this.doctype_select = this.wrapper.page.add_field({ + fieldname: "doctype_select", + label: __("Document Type"), + fieldtype: "Link", + options: "DocType", + change: function () { + frappe.set_route("permission-manager", this.get_value()); + }, + }); - this.role_select = this.wrapper.page - .add_select(__("Roles"), [__("Select Role") + "..."].concat(this.options.roles)) - .change(() => { - this.refresh(); - }); + this.role_select = this.wrapper.page.add_field({ + fieldname: "role_select", + label: __("Roles"), + fieldtype: "Link", + options: "Role", + change: () => this.refresh(), + }); this.page.add_inner_button(__("Set User Permissions"), () => { return frappe.set_route("List", "User Permission"); @@ -76,13 +77,13 @@ frappe.PermissionEngine = class PermissionEngine { return; } if (frappe.get_route()[1]) { - this.doctype_select.val(frappe.get_route()[1]); + this.doctype_select.set_value(frappe.get_route()[1]); } else if (frappe.route_options) { if (frappe.route_options.doctype) { - this.doctype_select.val(frappe.route_options.doctype); + this.doctype_select.set_value(frappe.route_options.doctype); } if (frappe.route_options.role) { - this.role_select.val(frappe.route_options.role); + this.role_select.set_value(frappe.route_options.role); } frappe.route_options = null; } @@ -140,13 +141,11 @@ frappe.PermissionEngine = class PermissionEngine { } get_doctype() { - let doctype = this.doctype_select.val(); - return this.doctype_select.get(0).selectedIndex == 0 ? null : doctype; + return this.doctype_select.get_value(); } get_role() { - let role = this.role_select.val(); - return this.role_select.get(0).selectedIndex == 0 ? null : role; + return this.role_select.get_value(); } set_empty_message(message) { @@ -292,6 +291,7 @@ frappe.PermissionEngine = class PermissionEngine { .attr("data-ptype", fieldname) .attr("data-role", d.role) .attr("data-permlevel", d.permlevel) + .attr("data-if_owner", d.if_owner) .attr("data-doctype", d.parent); checkbox.find("label").css("text-transform", "capitalize"); @@ -371,6 +371,7 @@ frappe.PermissionEngine = class PermissionEngine { doctype: d.parent, role: d.role, permlevel: d.permlevel, + if_owner: d.if_owner, }, callback: (r) => { if (r.exc) { @@ -399,6 +400,7 @@ frappe.PermissionEngine = class PermissionEngine { doctype: chk.attr("data-doctype"), ptype: chk.attr("data-ptype"), value: chk.prop("checked") ? 1 : 0, + if_owner: chk.attr("data-if_owner"), }; return frappe.call({ module: "frappe.core", diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 45dd5d7162..1368ced6eb 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -109,7 +109,7 @@ def add(parent, role, permlevel): @frappe.whitelist() -def update(doctype, role, permlevel, ptype, value=None): +def update(doctype, role, permlevel, ptype, value=None, if_owner=0): """Update role permission params Args: @@ -127,7 +127,7 @@ def update(doctype, role, permlevel, ptype, value=None): frappe.clear_cache(doctype=doctype) frappe.only_for("System Manager") - out = update_permission_property(doctype, role, permlevel, ptype, value) + out = update_permission_property(doctype, role, permlevel, ptype, value, if_owner=if_owner) frappe.db.after_commit.add(clear_cache) @@ -135,11 +135,14 @@ def update(doctype, role, permlevel, ptype, value=None): @frappe.whitelist() -def remove(doctype, role, permlevel): +def remove(doctype, role, permlevel, if_owner=0): frappe.only_for("System Manager") setup_custom_perms(doctype) - frappe.db.delete("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": permlevel}) + frappe.db.delete( + "Custom DocPerm", + {"parent": doctype, "role": role, "permlevel": permlevel, "if_owner": if_owner}, + ) if not frappe.get_all("Custom DocPerm", {"parent": doctype}): frappe.throw(_("There must be atleast one permission rule."), title=_("Cannot Remove")) diff --git a/frappe/desk/doctype/module_onboarding/module_onboarding.py b/frappe/desk/doctype/module_onboarding/module_onboarding.py index adb23074fb..1c7a2adeaf 100644 --- a/frappe/desk/doctype/module_onboarding/module_onboarding.py +++ b/frappe/desk/doctype/module_onboarding/module_onboarding.py @@ -52,7 +52,7 @@ class ModuleOnboarding(Document): is_complete = [bool(step.is_complete or step.is_skipped) for step in steps] if all(is_complete): self.is_complete = True - self.save() + self.save(ignore_permissions=True) return True return False diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index e48157db11..77767f589c 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import json +import typing from urllib.parse import quote import frappe @@ -14,6 +15,9 @@ from frappe.model.utils.user_settings import get_user_settings from frappe.permissions import get_doc_permissions from frappe.utils.data import cstr +if typing.TYPE_CHECKING: + from frappe.model.document import Document + @frappe.whitelist() def getdoc(doctype, name, user=None): @@ -117,7 +121,7 @@ def get_docinfo(doc=None, doctype=None, name=None): "assignments": get_assignments(doc.doctype, doc.name), "permissions": get_doc_permissions(doc), "shared": get_docshares(doc), - "views": get_view_logs(doc.doctype, doc.name), + "views": get_view_logs(doc), "energy_point_logs": get_point_logs(doc.doctype, doc.name), "additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name), "milestones": get_milestones(doc.doctype, doc.name), @@ -149,29 +153,22 @@ def add_comments(doc, docinfo): ) for c in comments: - if c.comment_type == "Comment": - c.content = frappe.utils.markdown(c.content) - docinfo.comments.append(c) - - elif c.comment_type in ("Shared", "Unshared"): - docinfo.shared.append(c) - - elif c.comment_type in ("Assignment Completed", "Assigned"): - docinfo.assignment_logs.append(c) - - elif c.comment_type in ("Attachment", "Attachment Removed"): - docinfo.attachment_logs.append(c) - - elif c.comment_type in ("Info", "Edit", "Label"): - docinfo.info_logs.append(c) - - elif c.comment_type == "Like": - docinfo.like_logs.append(c) - - elif c.comment_type == "Workflow": - docinfo.workflow_logs.append(c) - - frappe.utils.add_user_info(c.owner, docinfo.user_info) + match c.comment_type: + case "Comment": + c.content = frappe.utils.markdown(c.content) + docinfo.comments.append(c) + case "Shared" | "Unshared": + docinfo.shared.append(c) + case "Assignment Completed" | "Assigned": + docinfo.assignment_logs.append(c) + case "Attachment" | "Attachment Removed": + docinfo.attachment_logs.append(c) + case "Info" | "Edit" | "Label": + docinfo.info_logs.append(c) + case "Like": + docinfo.like_logs.append(c) + case "Workflow": + docinfo.workflow_logs.append(c) return comments @@ -192,7 +189,9 @@ def get_attachments(dt, dn): ) -def get_versions(doc): +def get_versions(doc: "Document") -> list[dict]: + if not doc.meta.track_changes: + return [] return frappe.get_all( "Version", filters=dict(ref_doctype=doc.doctype, docname=doc.name), @@ -362,32 +361,29 @@ def run_onload(doc): doc.run_method("onload") -def get_view_logs(doctype, docname): +def get_view_logs(doc: "Document") -> list[dict]: """get and return the latest view logs if available""" - logs = [] - if getattr(frappe.get_meta(doctype), "track_views", None): - view_logs = frappe.get_all( - "View Log", - filters={ - "reference_doctype": doctype, - "reference_name": docname, - }, - fields=["name", "creation", "owner"], - order_by="creation desc", - ) + if not doc.meta.track_views: + return [] - if view_logs: - logs = view_logs - return logs + return frappe.get_all( + "View Log", + filters={ + "reference_doctype": doc.doctype, + "reference_name": doc.name, + }, + fields=["name", "creation", "owner"], + order_by="creation desc", + ) -def get_tags(doctype, name): - tags = [ - tag.tag - for tag in frappe.get_all( - "Tag Link", filters={"document_type": doctype, "document_name": name}, fields=["tag"] - ) - ] +def get_tags(doctype: str, name: str) -> str: + tags = frappe.get_all( + "Tag Link", + filters={"document_type": doctype, "document_name": name}, + fields=["tag"], + pluck="tag", + ) return ",".join(tags) @@ -478,17 +474,20 @@ def send_link_titles(link_titles): def update_user_info(docinfo): - for d in docinfo.communications: - frappe.utils.add_user_info(d.sender, docinfo.user_info) + users = set() - for d in docinfo.shared: - frappe.utils.add_user_info(d.user, docinfo.user_info) + users.update(d.sender for d in docinfo.communications) + users.update(d.user for d in docinfo.shared) + users.update(d.owner for d in docinfo.assignments) + users.update(d.owner for d in docinfo.views) + users.update(d.owner for d in docinfo.workflow_logs) + users.update(d.owner for d in docinfo.like_logs) + users.update(d.owner for d in docinfo.info_logs) + users.update(d.owner for d in docinfo.attachment_logs) + users.update(d.owner for d in docinfo.assignment_logs) + users.update(d.owner for d in docinfo.comments) - for d in docinfo.assignments: - frappe.utils.add_user_info(d.owner, docinfo.user_info) - - for d in docinfo.views: - frappe.utils.add_user_info(d.owner, docinfo.user_info) + frappe.utils.add_user_info(users, docinfo.user_info) @frappe.whitelist() diff --git a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json index 0f936abae0..afd0583cfb 100644 --- a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json +++ b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json @@ -8,7 +8,7 @@ "include_name_field": 0, "is_standard": 1, "list_name": "", - "modified": "2023-08-24 11:01:18.688875", + "modified": "2023-05-24 12:43:43.741781", "modified_by": "Administrator", "module": "Desk", "name": "Main Workspace Tour", @@ -22,7 +22,7 @@ "steps": [ { "description": "This is Awesomebar, it helps you to navigate anywhere in the system, find documents, reports, settings, create new records and many more things.", - "element_selector": "#navbar-search[aria-expanded=\"true\"]", + "element_selector": "#navbar-search", "fieldtype": "0", "has_next_condition": 0, "hide_buttons": 0, diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index dd07dced40..ce3d4ee3e7 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -25,8 +25,7 @@ def get_report_doc(report_name): if doc.report_type == "Custom Report": custom_report_doc = doc - reference_report = custom_report_doc.reference_report - doc = frappe.get_doc("Report", reference_report) + doc = get_reference_report(doc) doc.custom_report = report_name if custom_report_doc.json: data = json.loads(custom_report_doc.json) @@ -172,9 +171,17 @@ def get_script(report_name): "html_format": html_format, "execution_time": frappe.cache.hget("report_execution_time", report_name) or 0, "filters": report.filters, + "custom_report_name": report.name if report.get("is_custom_report") else None, } +def get_reference_report(report): + if report.report_type != "Custom Report": + return report + reference_report = frappe.get_doc("Report", report.reference_report) + return get_reference_report(reference_report) + + @frappe.whitelist() @frappe.read_only() def run( diff --git a/frappe/migrate.py b/frappe/migrate.py index 3241b14152..6b83521a7f 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -135,6 +135,7 @@ class SiteMigration: sync_customizations() sync_languages() flush_deferred_inserts() + frappe.model.sync.remove_orphan_doctypes() frappe.get_single("Portal Settings").sync_menu() frappe.get_single("Installed Applications").update_versions() diff --git a/frappe/model/document.py b/frappe/model/document.py index c23e78cd38..d419abc727 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -216,7 +216,7 @@ class Document(BaseDocument): def check_permission(self, permtype="read", permlevel=None): """Raise `frappe.PermissionError` if not permitted""" if not self.has_permission(permtype): - self.raise_no_permission_to(permlevel or permtype) + self.raise_no_permission_to(permtype) def has_permission(self, permtype="read") -> bool: """ diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 0b344b892a..267a1667b5 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -7,6 +7,8 @@ import os import frappe +from frappe.cache_manager import clear_controller_cache +from frappe.model.base_document import get_controller from frappe.modules.import_file import import_file_by_path from frappe.modules.patch_handler import _patch_mode from frappe.utils import update_progress_bar @@ -135,3 +137,37 @@ def get_doc_files(files, start_path): files.append(doc_path) return files + + +def remove_orphan_doctypes(): + """Find and remove any orphaned doctypes. + + These are doctypes for which code and schema file is + deleted but entry is present in DocType table. + + Note: Deleting the entry doesn't delete any data. + So this is supposed to be non-destrictive operation. + """ + + doctype_names = frappe.get_all("DocType", {"custom": 0}, pluck="name") + orphan_doctypes = [] + + clear_controller_cache() + + for doctype in doctype_names: + try: + get_controller(doctype=doctype) + except ImportError: + orphan_doctypes.append(doctype) + except Exception: + continue + + if not orphan_doctypes: + return + + print(f"Orphaned DocType(s) found: {', '.join(orphan_doctypes)}") + for i, name in enumerate(orphan_doctypes): + frappe.delete_doc("DocType", name, force=True, ignore_missing=True) + update_progress_bar("Deleting orphaned DocTypes", i, len(orphan_doctypes)) + frappe.db.commit() + print() diff --git a/frappe/patches.txt b/frappe/patches.txt index 0a96030dcb..83db10d905 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -228,6 +228,5 @@ frappe.patches.v15_0.remove_background_jobs_from_dropdown frappe.desk.doctype.form_tour.patches.introduce_ui_tours execute:frappe.delete_doc_if_exists("Workspace", "Customization") execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_naming", "Amend Counter") -execute:frappe.delete_doc_if_exists("DocType", "Error Snapshot") frappe.patches.v15_0.move_event_cancelled_to_status frappe.patches.v15_0.set_file_type diff --git a/frappe/permissions.py b/frappe/permissions.py index a9f1b3e464..0e7c0d6480 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -546,13 +546,23 @@ def can_export(doctype, raise_exception=False): return has_access -def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True): +def update_permission_property( + doctype, + role, + permlevel, + ptype, + value=None, + validate=True, + if_owner=0, +): """Update a property in Custom Perm""" from frappe.core.doctype.doctype.doctype import validate_permissions_for_doctype out = setup_custom_perms(doctype) - name = frappe.db.get_value("Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel)) + name = frappe.db.get_value( + "Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel, if_owner=if_owner) + ) table = DocType("Custom DocPerm") frappe.qb.update(table).set(ptype, value).where(table.name == name).run() @@ -579,6 +589,12 @@ def add_permission(doctype, role, permlevel=0, ptype=None): if frappe.db.get_value( "Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel, if_owner=0) ): + frappe.msgprint( + _("Rule for this doctype, role, permlevel and if-owner combination already exists.").format( + doctype, + ), + alert=True, + ) return if not ptype: diff --git a/frappe/public/icons/timeless/icons.svg b/frappe/public/icons/timeless/icons.svg index 0436f199c1..e689bc4f65 100644 --- a/frappe/public/icons/timeless/icons.svg +++ b/frappe/public/icons/timeless/icons.svg @@ -120,6 +120,12 @@ + + + + + + @@ -302,7 +308,7 @@ - + diff --git a/frappe/public/js/frappe/file_uploader/TreeNode.vue b/frappe/public/js/frappe/file_uploader/TreeNode.vue index 308bb2b825..9b9f673ec9 100644 --- a/frappe/public/js/frappe/file_uploader/TreeNode.vue +++ b/frappe/public/js/frappe/file_uploader/TreeNode.vue @@ -1,22 +1,40 @@