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.searchmail.activity
@@ -235,6 +259,25 @@
+
+ mail.activity.view.tree.without.record.access
+ mail.activity
+
+ primary
+ 32
+
+
+ 0
+ 0
+ 0
+ archive_disabled_activity_list
+
+
+
+
+
+
+
tree
@@ -247,6 +290,33 @@
+
+ Other activities
+ mail.activity
+ tree,form
+
+ [('id', 'in', active_ids)]
+
+
+ No activities.
+
+
+
+
+
+ 1
+ tree
+
+
+
+
+
+ 2
+ form
+
+
+
+
mail.activity.view.calendarmail.activity
diff --git a/addons/test_mail/tests/test_mail_activity.py b/addons/test_mail/tests/test_mail_activity.py
index f64a2ba6edd..aae2d186f37 100644
--- a/addons/test_mail/tests/test_mail_activity.py
+++ b/addons/test_mail/tests/test_mail_activity.py
@@ -141,6 +141,26 @@ class TestActivityRights(TestActivityCommon):
'test_mail.mail_act_test_todo',
user_id=self.user_admin.id)
+ test_activity.user_id = self.user_employee
+ test_activity.flush_recordset()
+
+ # user can read activities assigned to him even if he has no access to the document
+ with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
+ found = self.env['mail.activity'].with_user(self.user_employee).search(
+ [('id', '=', test_activity.id)])
+ self.assertEqual(found, test_activity)
+ found.read(['summary'])
+
+ # user can read_group activities assigned to him even if he has no access to the document
+ with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash):
+ read_group_result = self.env['mail.activity'].with_user(self.user_employee).read_group(
+ [('id', '=', test_activity.id)],
+ ['summary'],
+ ['summary'],
+ )
+ self.assertEqual(1, read_group_result[0]['summary_count'])
+ self.assertEqual('Summary', read_group_result[0]['summary'])
+
@tests.tagged('mail_activity')
class TestActivityFlow(TestActivityCommon):