diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index d9bf4f70fa..ba26f8affa 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -399,7 +399,7 @@ def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=F for column in data.columns: if column.get("hidden"): continue - result[0].append(column.get("label")) + result[0].append(_(column.get("label"))) column_width = cint(column.get('width', 0)) # to convert into scale accepted by openpyxl column_width /= 10 diff --git a/frappe/hooks.py b/frappe/hooks.py index 4895c97200..be1b0134c1 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -221,7 +221,8 @@ scheduler_events = { "frappe.deferred_insert.save_to_db", "frappe.desk.form.document_follow.send_hourly_updates", "frappe.integrations.doctype.google_calendar.google_calendar.sync", - "frappe.email.doctype.newsletter.newsletter.send_scheduled_email" + "frappe.email.doctype.newsletter.newsletter.send_scheduled_email", + "frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.process_data_deletion_request" ], "daily": [ "frappe.email.queue.set_expiry_for_email_queue", @@ -240,8 +241,7 @@ scheduler_events = { "frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed", "frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails", "frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports", - "frappe.core.doctype.log_settings.log_settings.run_log_clean_up", - "frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.process_data_deletion_request" + "frappe.core.doctype.log_settings.log_settings.run_log_clean_up" ], "daily_long": [ "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", diff --git a/frappe/model/document.py b/frappe/model/document.py index dc0fd2caf0..3c38ff3442 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1003,8 +1003,6 @@ class Document(BaseDocument): - `on_cancel` for **Cancel** - `update_after_submit` for **Update after Submit**""" - doc_before_save = self.get_doc_before_save() - if self._action=="save": self.run_method("on_update") elif self._action=="submit": diff --git a/frappe/patches.txt b/frappe/patches.txt index c889d9a4da..a666480c90 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -197,3 +197,4 @@ frappe.patches.v14_0.copy_mail_data #08.03.21 frappe.patches.v14_0.update_github_endpoints #08-11-2021 frappe.patches.v14_0.remove_db_aggregation frappe.patches.v14_0.update_color_names_in_kanban_board_column +frappe.patches.v14_0.update_auto_account_deletion_duration diff --git a/frappe/patches/v14_0/update_auto_account_deletion_duration.py b/frappe/patches/v14_0/update_auto_account_deletion_duration.py new file mode 100644 index 0000000000..74957066e6 --- /dev/null +++ b/frappe/patches/v14_0/update_auto_account_deletion_duration.py @@ -0,0 +1,5 @@ +import frappe + +def execute(): + days = frappe.db.get_single_value("Website Settings", "auto_account_deletion") + frappe.db.set_value("Website Settings", None, "auto_account_deletion", days * 24) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 7c12809fcd..4c4e02bf41 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1343,7 +1343,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { if (file_format === 'CSV') { const column_row = this.columns.reduce((acc, col) => { if (!col.hidden) { - acc.push(col.label); + acc.push(__(col.label)); } return acc; }, []); diff --git a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py index 3699cdfbbd..e2f583fd48 100644 --- a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py +++ b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py @@ -7,7 +7,7 @@ import re import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import get_fullname, date_diff, get_datetime +from frappe.utils import get_fullname, time_diff_in_hours, get_datetime from frappe.utils.user import get_system_managers from frappe.utils.verified_command import get_signed_params, verify_request import json @@ -353,8 +353,8 @@ def process_data_deletion_request(): for request in requests: doc = frappe.get_doc("Personal Data Deletion Request", request) - if date_diff(get_datetime(), doc.creation) >= auto_account_deletion: - doc.add_comment("Comment", _("The User record for this request has been auto-deleted due to inactivity.")) + if time_diff_in_hours(get_datetime(), doc.creation) >= auto_account_deletion: + doc.add_comment("Comment", _("The User record for this request has been auto-deleted due to inactivity by system admins.")) doc.trigger_data_deletion() def remove_unverified_record(): diff --git a/frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py b/frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py index 27dcfe5858..675a891130 100644 --- a/frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py +++ b/frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py @@ -4,10 +4,10 @@ import frappe import unittest from frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request import ( - remove_unverified_record, + remove_unverified_record, process_data_deletion_request ) from frappe.website.doctype.personal_data_download_request.test_personal_data_download_request import ( - create_user_if_not_exists, + create_user_if_not_exists ) from datetime import datetime, timedelta @@ -58,3 +58,15 @@ class TestPersonalDataDeletionRequest(unittest.TestCase): self.assertFalse( frappe.db.exists("Personal Data Deletion Request", self.delete_request.name) ) + + def test_process_auto_request(self): + frappe.db.set_value("Website Settings", None, "auto_account_deletion", "1") + date_time_obj = datetime.strptime( + self.delete_request.creation, "%Y-%m-%d %H:%M:%S.%f" + ) + timedelta(hours=-2) + self.delete_request.db_set("creation", date_time_obj) + self.delete_request.db_set("status", "Pending Approval") + + process_data_deletion_request() + self.delete_request.reload() + self.assertEqual(self.delete_request.status, "Deleted") diff --git a/frappe/website/doctype/website_settings/website_settings.json b/frappe/website/doctype/website_settings/website_settings.json index 2a6b3dc1fb..3b199a4b58 100644 --- a/frappe/website/doctype/website_settings/website_settings.json +++ b/frappe/website/doctype/website_settings/website_settings.json @@ -404,10 +404,10 @@ "label": "Show Account Deletion Link in My Account Page" }, { - "default": "3", + "default": "72", "fieldname": "auto_account_deletion", "fieldtype": "Int", - "label": "Auto Account Deletion within (Days)" + "label": "Auto Account Deletion within (Hours)" }, { "fieldname": "footer_powered", @@ -421,7 +421,7 @@ "issingle": 1, "links": [], "max_attachments": 10, - "modified": "2022-02-28 23:05:42.493192", + "modified": "2022-02-24 15:37:22.360138", "modified_by": "Administrator", "module": "Website", "name": "Website Settings", @@ -446,4 +446,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js b/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js index 1b9e9ad79b..731fe29cef 100644 --- a/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js +++ b/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js @@ -5,7 +5,7 @@ frappe.ready(function() { callback: (data) => { if (data.message) { const intro_wrapper = $('#introduction .ql-editor.read-mode'); - const sla_description = __("Note: Your request for account deletion will be fulfilled within {0} days.", [data.message]); + const sla_description = __("Note: Your request for account deletion will be fulfilled within {0} hours.", [data.message]); const sla_description_wrapper = `
${sla_description}`; intro_wrapper.html(intro_wrapper.html() + sla_description_wrapper); } diff --git a/frappe/workflow/doctype/workflow/test_workflow.py b/frappe/workflow/doctype/workflow/test_workflow.py index d2d85e696b..14ecdfb5a1 100644 --- a/frappe/workflow/doctype/workflow/test_workflow.py +++ b/frappe/workflow/doctype/workflow/test_workflow.py @@ -5,6 +5,7 @@ import unittest from frappe.utils import random_string from frappe.model.workflow import apply_workflow, WorkflowTransitionError, WorkflowPermissionError, get_common_transition_actions from frappe.test_runner import make_test_records +from frappe.query_builder import DocType class TestWorkflow(unittest.TestCase): @@ -15,9 +16,31 @@ class TestWorkflow(unittest.TestCase): def setUp(self): self.workflow = create_todo_workflow() frappe.set_user('Administrator') + if self._testMethodName == "test_if_workflow_actions_were_processed_using_user": + if not frappe.db.has_column('Workflow Action', 'user'): + # mariadb would raise this statement would create an implicit commit + # if we do not commit before alter statement + # nosemgrep + frappe.db.commit() + frappe.db.multisql({ + 'mariadb': 'ALTER TABLE `tabWorkflow Action` ADD COLUMN user varchar(140)', + 'postgres': 'ALTER TABLE "tabWorkflow Action" ADD COLUMN "user" varchar(140)' + }) + frappe.cache().delete_value('table_columns') def tearDown(self): frappe.delete_doc('Workflow', 'Test ToDo') + if self._testMethodName == "test_if_workflow_actions_were_processed_using_user": + if frappe.db.has_column('Workflow Action', 'user'): + # mariadb would raise this statement would create an implicit commit + # if we do not commit before alter statement + # nosemgrep + frappe.db.commit() + frappe.db.multisql({ + 'mariadb': 'ALTER TABLE `tabWorkflow Action` DROP COLUMN user', + 'postgres': 'ALTER TABLE "tabWorkflow Action" DROP COLUMN "user"' + }) + frappe.cache().delete_value('table_columns') def test_default_condition(self): '''test default condition is set''' @@ -75,7 +98,7 @@ class TestWorkflow(unittest.TestCase): actions = get_common_transition_actions([todo1, todo2], 'ToDo') self.assertListEqual(actions, ['Review']) - def test_if_workflow_actions_were_processed(self): + def test_if_workflow_actions_were_processed_using_role(self): frappe.db.delete("Workflow Action") user = frappe.get_doc('User', 'test2@example.com') user.add_roles('Test Approver', 'System Manager') @@ -93,6 +116,32 @@ class TestWorkflow(unittest.TestCase): self.assertEqual(workflow_actions[0].status, 'Completed') frappe.set_user('Administrator') + def test_if_workflow_actions_were_processed_using_user(self): + frappe.db.delete("Workflow Action") + + user = frappe.get_doc('User', 'test2@example.com') + user.add_roles('Test Approver', 'System Manager') + frappe.set_user('test2@example.com') + + doc = self.test_default_condition() + workflow_actions = frappe.get_all('Workflow Action', fields=['*']) + self.assertEqual(len(workflow_actions), 1) + + # test if status of workflow actions are updated on approval + WorkflowAction = DocType("Workflow Action") + WorkflowActionPermittedRole = DocType("Workflow Action Permitted Role") + frappe.qb.update(WorkflowAction).set(WorkflowAction.user, 'test2@example.com').run() + frappe.qb.update(WorkflowActionPermittedRole).set(WorkflowActionPermittedRole.role, '').run() + + self.test_approve(doc) + + user.remove_roles('Test Approver', 'System Manager') + workflow_actions = frappe.get_all('Workflow Action', fields=['status']) + self.assertEqual(len(workflow_actions), 1) + self.assertEqual(workflow_actions[0].status, 'Completed') + frappe.set_user('Administrator') + + def test_update_docstatus(self): todo = create_new_todo() apply_workflow(todo, 'Approve') diff --git a/frappe/workflow/doctype/workflow_action/workflow_action.json b/frappe/workflow/doctype/workflow_action/workflow_action.json index f1290d001f..aeb60feceb 100644 --- a/frappe/workflow/doctype/workflow_action/workflow_action.json +++ b/frappe/workflow/doctype/workflow_action/workflow_action.json @@ -8,9 +8,12 @@ "status", "reference_name", "reference_doctype", - "user", "workflow_state", - "completed_by" + "column_break_5", + "completed_by_role", + "completed_by", + "permitted_roles", + "user" ], "fields": [ { @@ -24,16 +27,14 @@ "fieldname": "reference_name", "fieldtype": "Dynamic Link", "label": "Reference Name", - "options": "reference_doctype", - "search_index": 1 + "options": "reference_doctype" }, { "fieldname": "reference_doctype", "fieldtype": "Link", "in_list_view": 1, "label": "Reference Document Type", - "options": "DocType", - "search_index": 1 + "options": "DocType" }, { "fieldname": "user", @@ -47,18 +48,38 @@ "fieldname": "workflow_state", "fieldtype": "Data", "hidden": 1, - "label": "Workflow State", - "search_index": 1 + "label": "Workflow State" }, { + "depends_on": "eval: doc.completed_by", "fieldname": "completed_by", "fieldtype": "Link", - "label": "Completed By", - "options": "User" + "label": "Completed By User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.completed_by_role", + "fieldname": "completed_by_role", + "fieldtype": "Link", + "label": "Completed By Role", + "options": "Role", + "read_only": 1 + }, + { + "fieldname": "permitted_roles", + "fieldtype": "Table MultiSelect", + "label": "Permitted Roles", + "options": "Workflow Action Permitted Role", + "read_only": 1 } ], "links": [], - "modified": "2021-07-01 09:07:52.848618", + "modified": "2022-02-23 21:06:45.122258", "modified_by": "Administrator", "module": "Workflow", "name": "Workflow Action", @@ -72,6 +93,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "reference_name", "track_changes": 1 } \ No newline at end of file diff --git a/frappe/workflow/doctype/workflow_action/workflow_action.py b/frappe/workflow/doctype/workflow_action/workflow_action.py index c8561fe922..0ab8924a6b 100644 --- a/frappe/workflow/doctype/workflow_action/workflow_action.py +++ b/frappe/workflow/doctype/workflow_action/workflow_action.py @@ -13,24 +13,46 @@ from frappe.model.workflow import apply_workflow, get_workflow_name, has_approva from frappe.desk.notifications import clear_doctype_notifications from frappe.utils.user import get_users_with_role from frappe.utils.data import get_link_to_form +from frappe.query_builder import DocType class WorkflowAction(Document): pass def on_doctype_update(): - frappe.db.add_index("Workflow Action", ["status", "user"]) + # The search order in any use case is no ["reference_name", "reference_doctype", "status"] + # The index scan would happen from left to right + # so even if status is not in the where clause the index will be used + frappe.db.add_index("Workflow Action", ["reference_name", "reference_doctype", "status"]) def get_permission_query_conditions(user): if not user: user = frappe.session.user if user == "Administrator": return "" - return "(`tabWorkflow Action`.`user`='{user}')".format(user=user) + roles = frappe.get_roles(user) + + WorkflowAction = DocType("Workflow Action") + WorkflowActionPermittedRole = DocType("Workflow Action Permitted Role") + + permitted_workflow_actions = (frappe.qb.from_(WorkflowAction) + .join(WorkflowActionPermittedRole) + .on(WorkflowAction.name == WorkflowActionPermittedRole.parent) + .select(WorkflowAction.name) + .where(WorkflowActionPermittedRole.role.isin(roles)) + ).get_sql() + + return f"""(`tabWorkflow Action`.`name` in ({permitted_workflow_actions}) + or `tabWorkflow Action`.`user`='{user}') + and `tabWorkflow Action`.`status`='Open'""" def has_permission(doc, user): - if user not in ['Administrator', doc.user]: - return False + + user_roles = set(frappe.get_roles(user)) + + permitted_roles = {permitted_role.role for permitted_role in doc.permitted_roles} + + return user == "Administrator" or user_roles.intersection(permitted_roles) def process_workflow_actions(doc, state): workflow = get_workflow_name(doc.get('doctype')) @@ -42,19 +64,18 @@ def process_workflow_actions(doc, state): if is_workflow_action_already_created(doc): return - clear_old_workflow_actions(doc) - update_completed_workflow_actions(doc) + update_completed_workflow_actions(doc, workflow=workflow, workflow_state=get_doc_workflow_state(doc)) clear_doctype_notifications('Workflow Action') next_possible_transitions = get_next_possible_transitions(workflow, get_doc_workflow_state(doc), doc) if not next_possible_transitions: return - user_data_map = get_users_next_action_data(next_possible_transitions, doc) + user_data_map, roles = get_users_next_action_data(next_possible_transitions, doc) if not user_data_map: return - create_workflow_actions_for_users(user_data_map.keys(), doc) + create_workflow_actions_for_roles(roles, doc) if send_email_alert(workflow): enqueue(send_workflow_action_email, queue='short', users_data=list(user_data_map.values()), doc=doc) @@ -132,20 +153,85 @@ def return_link_expired_page(doc, doc_workflow_state): frappe.bold(frappe.get_value('User', doc.get("modified_by"), 'full_name')) ), indicator_color='blue') -def clear_old_workflow_actions(doc, user=None): + +def update_completed_workflow_actions(doc, user=None, workflow=None, workflow_state=None): + allowed_roles = get_allowed_roles(user, workflow, workflow_state) + # There is no transaction leading upto this state + # so no older actions to complete + if not allowed_roles: + return + if workflow_action := get_workflow_action_by_role(doc, allowed_roles): + update_completed_workflow_actions_using_role(doc, user, allowed_roles, workflow_action) + else: + # backwards compatibility + # for workflow actions saved using user + clear_old_workflow_actions_using_user(doc, user) + update_completed_workflow_actions_using_user(doc, user) + +def get_allowed_roles(user, workflow, workflow_state): user = user if user else frappe.session.user - frappe.db.delete("Workflow Action", { - "reference_doctype": doc.get("doctype"), - "reference_name": doc.get("name"), - "user": ("!=", user), - "status": "Open" - }) -def update_completed_workflow_actions(doc, user=None): + allowed_roles = frappe.get_all('Workflow Transition', + fields='allowed', + filters=[ + ['parent', '=', workflow], + ['next_state', '=', workflow_state] + ], + pluck = 'allowed') + + user_roles = set(frappe.get_roles(user)) + return set(allowed_roles).intersection(user_roles) + +def get_workflow_action_by_role(doc, allowed_roles): + WorkflowAction = DocType("Workflow Action") + WorkflowActionPermittedRole = DocType("Workflow Action Permitted Role") + return (frappe.qb.from_(WorkflowAction).join(WorkflowActionPermittedRole) + .on(WorkflowAction.name == WorkflowActionPermittedRole.parent) + .select(WorkflowAction.name, WorkflowActionPermittedRole.role) + .where((WorkflowAction.reference_name == doc.get('name')) + & (WorkflowAction.reference_doctype == doc.get('doctype')) + & (WorkflowAction.status == 'Open') + & (WorkflowActionPermittedRole.role.isin(list(allowed_roles)))) + .orderby(WorkflowActionPermittedRole.role).limit(1)).run(as_dict=True) + +def update_completed_workflow_actions_using_role(doc, user=None, allowed_roles = set(), workflow_action=None): user = user if user else frappe.session.user - frappe.db.sql("""UPDATE `tabWorkflow Action` SET `status`='Completed', `completed_by`=%s - WHERE `reference_doctype`=%s AND `reference_name`=%s AND `user`=%s AND `status`='Open'""", - (user, doc.get('doctype'), doc.get('name'), user)) + WorkflowAction = DocType("Workflow Action") + + if not workflow_action: + return + + (frappe.qb.update(WorkflowAction) + .set(WorkflowAction.status, 'Completed') + .set(WorkflowAction.completed_by, user) + .set(WorkflowAction.completed_by_role, workflow_action[0].role) + .where(WorkflowAction.name == workflow_action[0].name) + ).run() + +def clear_old_workflow_actions_using_user(doc, user=None): + user = user if user else frappe.session.user + + if frappe.db.has_column('Workflow Action', 'user'): + frappe.db.delete("Workflow Action", { + "reference_name": doc.get("name"), + "reference_doctype": doc.get("doctype"), + "status": "Open", + "user": ("!=", user) + }) + +def update_completed_workflow_actions_using_user(doc, user=None): + user = user or frappe.session.user + + if frappe.db.has_column('Workflow Action', 'user'): + WorkflowAction = DocType("Workflow Action") + (frappe.qb.update(WorkflowAction) + .set(WorkflowAction.status, 'Completed') + .set(WorkflowAction.completed_by, user) + .where((WorkflowAction.reference_name == doc.get('name')) + & (WorkflowAction.reference_doctype == doc.get('doctype')) + & (WorkflowAction.status == 'Open') + & (WorkflowAction.user == user)) + ).run() def get_next_possible_transitions(workflow_name, state, doc=None): transitions = frappe.get_all('Workflow Transition', @@ -167,8 +253,10 @@ def get_next_possible_transitions(workflow_name, state, doc=None): return transitions_to_return def get_users_next_action_data(transitions, doc): + roles = set() user_data_map = {} for transition in transitions: + roles.add(transition.allowed) users = get_users_with_role(transition.allowed) filtered_users = filter_allowed_users(users, doc, transition) for user in filtered_users: @@ -182,19 +270,24 @@ def get_users_next_action_data(transitions, doc): 'action_name': transition.action, 'action_link': get_workflow_action_url(transition.action, doc, user) })) - return user_data_map + return user_data_map, roles -def create_workflow_actions_for_users(users, doc): - for user in users: - frappe.get_doc({ - 'doctype': 'Workflow Action', - 'reference_doctype': doc.get('doctype'), - 'reference_name': doc.get('name'), - 'workflow_state': get_doc_workflow_state(doc), - 'status': 'Open', - 'user': user - }).insert(ignore_permissions=True) +def create_workflow_actions_for_roles(roles, doc): + workflow_action = frappe.get_doc({ + 'doctype': 'Workflow Action', + 'reference_doctype': doc.get('doctype'), + 'reference_name': doc.get('name'), + 'workflow_state': get_doc_workflow_state(doc), + 'status': 'Open', + }) + + for role in roles: + workflow_action.append('permitted_roles', { + 'role': role + }) + + workflow_action.insert(ignore_permissions=True) def send_workflow_action_email(users_data, doc): common_args = get_common_email_args(doc) @@ -249,18 +342,20 @@ def get_confirm_workflow_action_url(doc, action, user): def is_workflow_action_already_created(doc): return frappe.db.exists({ 'doctype': 'Workflow Action', - 'reference_doctype': doc.get('doctype'), 'reference_name': doc.get('name'), - 'workflow_state': get_doc_workflow_state(doc) + 'reference_doctype': doc.get('doctype'), + 'workflow_state': get_doc_workflow_state(doc), }) def clear_workflow_actions(doctype, name): if not (doctype and name): return - frappe.db.delete("Workflow Action", { - "reference_doctype": doctype, - "reference_name": name - }) + frappe.db.delete("Workflow Action", filters = { + "reference_name": name, + "reference_doctype": doctype, + } + ) + def get_doc_workflow_state(doc): workflow_name = get_workflow_name(doc.get('doctype')) workflow_state_field = get_workflow_state_field(workflow_name) diff --git a/frappe/workflow/doctype/workflow_action_permitted_role/__init__.py b/frappe/workflow/doctype/workflow_action_permitted_role/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.json b/frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.json new file mode 100644 index 0000000000..19b2dcba19 --- /dev/null +++ b/frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2022-02-21 20:28:05.662187", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "role" + ], + "fields": [ + { + "fieldname": "role", + "fieldtype": "Link", + "label": "Role", + "options": "Role" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-02-21 20:28:05.662187", + "modified_by": "Administrator", + "module": "Workflow", + "name": "Workflow Action Permitted Role", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.py b/frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.py new file mode 100644 index 0000000000..0370f6a4c8 --- /dev/null +++ b/frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.py @@ -0,0 +1,8 @@ +# Copyright (c) 2022, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class WorkflowActionPermittedRole(Document): + pass