diff --git a/addons/mail/i18n/mail.pot b/addons/mail/i18n/mail.pot index af5344b1b30..da5ad0af1e5 100644 --- a/addons/mail/i18n/mail.pot +++ b/addons/mail/i18n/mail.pot @@ -651,6 +651,7 @@ msgstr "" #: model_terms:ir.ui.view,arch_db:mail.mail_activity_plan_template_view_form #: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_calendar #: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_form_popup +#: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_form_without_record_access #: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_search #, python-format msgid "Activity" @@ -5163,11 +5164,13 @@ msgstr "" #. module: mail #: model_terms:ir.ui.view,arch_db:mail.mail_activity_schedule_view_form #: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_form_popup +#: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_form_without_record_access msgid "Log a note..." msgstr "" #. module: mail #: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_form_popup +#: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_form_without_record_access msgid "Log an Activity" msgstr "" @@ -5455,6 +5458,7 @@ msgstr "" #. module: mail #: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_form_popup +#: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_tree_without_record_access msgid "Mark as Done" msgstr "" @@ -6031,6 +6035,11 @@ msgstr "" msgid "No Record" msgstr "" +#. module: mail +#: model_terms:ir.actions.act_window,help:mail.mail_activity_without_access_action +msgid "No activities." +msgstr "" + #. module: mail #. odoo-javascript #: code:addons/mail/static/src/discuss/core/web/command_palette.js:0 @@ -10532,6 +10541,7 @@ msgstr "" #. module: mail #: model_terms:ir.ui.view,arch_db:mail.mail_activity_schedule_view_form #: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_form_popup +#: model_terms:ir.ui.view,arch_db:mail.mail_activity_view_form_without_record_access #: model_terms:ir.ui.view,arch_db:mail.view_server_action_form_template msgid "e.g. Discuss proposal" msgstr "" diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 7ac69f4dc8f..01256dfe141 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -218,6 +218,9 @@ class MailActivity(models.Model): valid = super(MailActivity, self)._filter_access_rules(operation) if valid and valid == self: return self + elif operation == 'read': + # Not in the ACL otherwise it would break the custom _search method + valid = self.sudo().filtered_domain([('user_id', '=', self.env.uid)]) else: valid = self.env[self._name] return self._filter_access_rules_remaining(valid, operation, '_filter_access_rules') @@ -228,6 +231,8 @@ class MailActivity(models.Model): valid = super(MailActivity, self)._filter_access_rules_python(operation) if valid and valid == self: return self + elif operation == 'read': + valid = self.sudo().filtered_domain([('user_id', '=', self.env.uid)]) else: valid = self.env[self._name] return self._filter_access_rules_remaining(valid, operation, '_filter_access_rules_python') @@ -413,14 +418,17 @@ class MailActivity(models.Model): f'"{self._table}"."id"', f'"{self._table}"."res_model"', f'"{self._table}"."res_id"', + f'"{self._table}"."user_id"', ) self.env.cr.execute(query_str, params) rows = self.env.cr.fetchall() # group res_ids by model, and determine accessible records + # Note: the user can read all activities assigned to him (see at the end of the method) model_ids = defaultdict(set) - for _id, res_model, res_id in rows: - model_ids[res_model].add(res_id) + for __, res_model, res_id, user_id in rows: + if user_id != self.env.uid: + model_ids[res_model].add(res_id) allowed_ids = defaultdict(set) for res_model, res_ids in model_ids.items(): @@ -433,8 +441,8 @@ class MailActivity(models.Model): activities = self.browse( id_ - for id_, res_model, res_id in rows - if res_id in allowed_ids[res_model] + for id_, res_model, res_id, user_id in rows + if user_id == self.env.uid or res_id in allowed_ids[res_model] ) return activities._as_query(order) @@ -544,17 +552,20 @@ class MailActivity(models.Model): activity_attachments[activity_id].append(attachment['id']) for model, activity_data in self._classify_by_model().items(): - records = self.env[model].browse(activity_data['record_ids']) - for record, activity in zip(records, activity_data['activities']): + # Allow user without access to the record to "mark as done" activities assigned to them. At the end of the + # method, the activity is unlinked or archived which ensure the user has enough right on the activities. + records_sudo = self.env[model].sudo().browse(activity_data['record_ids']) + for record_sudo, activity in zip(records_sudo, activity_data['activities']): # extract value to generate next activities if activity.chaining_type == 'trigger': vals = activity.with_context(activity_previous_deadline=activity.date_deadline)._prepare_next_activity_values() next_activities_values.append(vals) # post message on activity, before deleting it - activity_message = record.message_post_with_source( + activity_message = record_sudo.message_post_with_source( 'mail.message_activity_done', attachment_ids=attachment_ids, + author_id=self.env.user.partner_id.id, render_values={ 'activity': activity, 'feedback': feedback, diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py index 04a1cbb35ee..1169ddd9096 100644 --- a/addons/mail/models/res_users.py +++ b/addons/mail/models/res_users.py @@ -273,19 +273,29 @@ class Users(models.Model): @api.model def systray_get_activities(self): - activities = self.env["mail.activity"].search([("user_id", "=", self.env.uid)]) + search_limit = int(self.env['ir.config_parameter'].sudo().get_param('mail.activity.systray.limit', 1000)) + activities = self.env["mail.activity"].search( + [("user_id", "=", self.env.uid)], order='id desc', limit=search_limit) activities_by_record_by_model_name = defaultdict(lambda: defaultdict(lambda: self.env["mail.activity"])) for activity in activities: record = self.env[activity.res_model].browse(activity.res_id) activities_by_record_by_model_name[activity.res_model][record] += activity - model_ids = list({self.env["ir.model"]._get(name).id for name in activities_by_record_by_model_name.keys()}) - user_activities = {} + activities_by_model_name = defaultdict(lambda: self.env["mail.activity"]) for model_name, activities_by_record in activities_by_record_by_model_name.items(): - domain = [("id", "in", list({r.id for r in activities_by_record.keys()}))] + if self.env[model_name].check_access_rights('read', raise_exception=False): + res_ids = [r.id for r in activities_by_record] + allowed_records = self.env[model_name].browse(res_ids)._filter_access_rules('read') + else: + allowed_records = self.env[model_name] + for record, activities in activities_by_record.items(): + if record not in allowed_records: + activities_by_model_name['mail.activity'] += activities + else: + activities_by_model_name[model_name] += activities + model_ids = [self.env["ir.model"]._get_id(name) for name in activities_by_model_name] + user_activities = {} + for model_name, activities in activities_by_model_name.items(): Model = self.env[model_name] - allowed_records = Model.search(domain) - if not allowed_records: - continue module = Model._original_module icon = module and modules.module.get_module_icon(module) model = self.env["ir.model"]._get(model_name).with_prefetch(model_ids) @@ -301,11 +311,12 @@ class Users(models.Model): "planned_count": 0, "view_type": getattr(Model, '_systray_view', 'list'), } - for record, activities in activities_by_record.items(): - if record not in allowed_records: - continue - for activity in activities: - user_activities[model_name]["%s_count" % activity.state] += 1 - if activity.state in ("today", "overdue"): - user_activities[model_name]["total_count"] += 1 + if model_name == 'mail.activity': + user_activities[model_name]['activity_ids'] = activities.ids + for activity in activities: + user_activities[model_name]["%s_count" % activity.state] += 1 + if activity.state in ("today", "overdue"): + user_activities[model_name]["total_count"] += 1 + if "mail.activity" in user_activities: + user_activities["mail.activity"]["name"] = _("Other activities") return list(user_activities.values()) diff --git a/addons/mail/static/src/core/web/activity_menu.js b/addons/mail/static/src/core/web/activity_menu.js index 72aa7b2e191..1bacc0bcdcd 100644 --- a/addons/mail/static/src/core/web/activity_menu.js +++ b/addons/mail/static/src/core/web/activity_menu.js @@ -6,6 +6,7 @@ import { Dropdown } from "@web/core/dropdown/dropdown"; import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; import { Domain } from "@web/core/domain"; +import { RPCError } from "@web/core/network/rpc_service"; export class ActivityMenu extends Component { static components = { Dropdown }; @@ -31,8 +32,13 @@ export class ActivityMenu extends Component { this.sortActivityGroups(); } + /** + * Sort by model ID ASC but always place the activity group for "mail.activity" model at the end (other activities). + */ sortActivityGroups() { - this.store.activityGroups.sort((g1, g2) => g1.id - g2.id); + const getSortId = (activityGroup) => + activityGroup.model === "mail.activity" ? Number.MAX_VALUE : activityGroup.id; + this.store.activityGroups.sort((g1, g2) => getSortId(g1) - getSortId(g2)); } onBeforeOpen() { @@ -55,6 +61,24 @@ export class ActivityMenu extends Component { // So, duplicates are faking the count and "Load more" doesn't show up force_search_count: 1, }; + if (group.model === "mail.activity") { + this.action + .doAction("mail.mail_activity_without_access_action", { + additionalContext: { + active_ids: group.activity_ids, + }, + }) + .catch((error) => { + if (error instanceof RPCError) { + this.action.doAction("mail.mail_activity_action", { + additionalContext: { + active_ids: group.activity_ids, + }, + }); + } + }); + return; + } let domain = [["activity_user_id", "=", this.userId]]; if (group.domain) { domain = Domain.and([domain, group.domain]).toList(); diff --git a/addons/mail/static/src/views/web/list/archive_disabled_list_controller.js b/addons/mail/static/src/views/web/list/archive_disabled_list_controller.js new file mode 100644 index 00000000000..da0270c4bda --- /dev/null +++ b/addons/mail/static/src/views/web/list/archive_disabled_list_controller.js @@ -0,0 +1,10 @@ +/** @odoo-module */ + +import { ListController } from "@web/views/list/list_controller"; + +export class ArchiveDisabledListController extends ListController { + setup() { + super.setup(); + this.archiveEnabled = false; + } +} diff --git a/addons/mail/static/src/views/web/list/archive_disabled_list_view.js b/addons/mail/static/src/views/web/list/archive_disabled_list_view.js new file mode 100644 index 00000000000..cdd4c97d330 --- /dev/null +++ b/addons/mail/static/src/views/web/list/archive_disabled_list_view.js @@ -0,0 +1,12 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { listView } from "@web/views/list/list_view"; +import { ArchiveDisabledListController } from "./archive_disabled_list_controller"; + +export const archiveDisabledListView = { + ...listView, + Controller: ArchiveDisabledListController, +}; + +registry.category("views").add("archive_disabled_activity_list", archiveDisabledListView); diff --git a/addons/mail/views/mail_activity_views.xml b/addons/mail/views/mail_activity_views.xml index be0a6340b98..c82095e77c7 100644 --- a/addons/mail/views/mail_activity_views.xml +++ b/addons/mail/views/mail_activity_views.xml @@ -194,6 +194,30 @@ + + mail.activity.view.form.without.record.access + mail.activity + 32 + +
+ + + + + + + + + + + + + + +
+
+
+ mail.activity.view.search mail.activity @@ -235,6 +259,25 @@ + + mail.activity.view.tree.without.record.access + mail.activity + + primary + 32 + + + 0 + 0 + 0 + archive_disabled_activity_list + + +