From b6c236df54abb66b30d8643dea797f385d3036f1 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Mon, 9 Oct 2023 18:26:46 +0200 Subject: [PATCH 1/6] [IMP] {test_}mail: allow to keep done activities and improve activity view We add the option to keep the completed activities to allow the user to keep track of the past action, especially the uploaded files which are displayed along the completed activities. This feature can be activated per activity type and is disabled by default. We also improve the activity view to give a better overview of what has been done and what is still to be done and by whom. We also add tests for those new features: - check activity data for the views (server-side test) * Details about the activity view improvements: ** Improvement of the column header: - progressbar: previously, only visible activities were counted in the progression bar. Now all are counted. - activity counter: instead of displaying the total count (next to the progression bar), we display now a ratio as 2 numbers (only if keep done is enabled): ** Improvement of the activity cells: - add assignees in activity cells of activity view (limit to 2 having the activity with closest deadline, if more a + is displayed). Note: to avoid spacing between avatar, we add the optional props noSpacing to the avatar component. - add done activity in activity cell popover - increase by 50% the popover height - display done activity cell in grey - instead of displaying the activity count if greater than 1, we display a ratio ongoing / activity count. Note: we don't display the denominator if all activities are done or all are ongoing. ** Progress bar filter The activity filter is slighly modified. Previously, when clicking on a color of the progress bar, all cell with a different color where hidden (ex.: if we were clicking on the green area to filter planned activities, red cell which represents overdue activities were hidden even if there was also a planned activities for this record and activity type). Now we also display cells with other "color" if there exist for that record and that type an activity corresponding to the filtered "color". Technical note: mail_activity.get_activity_data returns now for the activity types a list of dictionary instead of a list of list. This simplifies the reading as we can now uses name instead of index (type[1] becomes type.name for example). Task-3300854 Part-of: odoo/odoo#138135 --- addons/mail/models/mail_activity.py | 207 ++++++++++++++---- addons/mail/models/mail_activity_type.py | 1 + addons/mail/models/mail_thread.py | 2 +- .../src/core/web/activity_list_popover.js | 4 + .../src/core/web/activity_list_popover.xml | 8 + .../core/web/activity_list_popover_item.js | 26 ++- .../core/web/activity_list_popover_item.xml | 11 +- .../src/core/web/activity_list_view.scss | 2 +- .../static/src/core/web/activity_model.js | 7 + .../src/core/web/mail_column_progress.js | 12 + .../src/core/web/mail_column_progress.xml | 13 ++ .../src/views/web/activity/activity_cell.js | 43 ++-- .../src/views/web/activity/activity_cell.scss | 3 - .../src/views/web/activity/activity_cell.xml | 39 +++- .../web/activity/activity_controller.scss | 33 +++ .../src/views/web/activity/activity_model.js | 1 + .../views/web/activity/activity_renderer.js | 96 +++++--- .../views/web/activity/activity_renderer.xml | 47 ++-- .../src/views/web/fields/avatar/avatar.js | 7 +- .../src/views/web/fields/avatar/avatar.xml | 4 +- addons/mail/views/mail_activity_views.xml | 1 + addons/test_mail/data/data.xml | 6 + addons/test_mail/tests/test_mail_activity.py | 162 +++++++++++++- 23 files changed, 608 insertions(+), 127 deletions(-) create mode 100644 addons/mail/static/src/core/web/mail_column_progress.js create mode 100644 addons/mail/static/src/core/web/mail_column_progress.xml delete mode 100644 addons/mail/static/src/views/web/activity/activity_cell.scss diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 9e295b98151..c6b310f3c67 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -4,14 +4,14 @@ import logging import pytz -from collections import defaultdict +from collections import defaultdict, Counter from datetime import date, datetime from dateutil.relativedelta import relativedelta from odoo import api, exceptions, fields, models, _, Command from odoo.osv import expression from odoo.tools import is_html_empty -from odoo.tools.misc import clean_context, get_lang +from odoo.tools.misc import clean_context, get_lang, groupby _logger = logging.getLogger(__name__) @@ -78,9 +78,15 @@ class MailActivity(models.Model): summary = fields.Char('Summary') note = fields.Html('Note', sanitize_style=True) date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.context_today) + date_done = fields.Date('Done Date', compute='_compute_date_done', store=True) automated = fields.Boolean( 'Automated activity', readonly=True, help='Indicates this activity has been created automatically and not by any user.') + # Attachments are linked to a document through model / res_id and to the activity through this field. + attachment_ids = fields.Many2many( + 'ir.attachment', 'activity_attachment_rel', + 'activity_id', 'attachment_id', + string='Attachments') # description user_id = fields.Many2one( 'res.users', 'Assigned to', @@ -90,7 +96,8 @@ class MailActivity(models.Model): state = fields.Selection([ ('overdue', 'Overdue'), ('today', 'Today'), - ('planned', 'Planned')], 'State', + ('planned', 'Planned'), + ('done', 'Done')], 'State', compute='_compute_state') recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type") previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type', readonly=True) @@ -101,6 +108,7 @@ class MailActivity(models.Model): chaining_type = fields.Selection(related='activity_type_id.chaining_type', readonly=True) # access can_write = fields.Boolean(compute='_compute_can_write') # used to hide buttons if the current user has no access + active = fields.Boolean(default=True) _sql_constraints = [ # Required on a Many2one reference field is not sufficient as actually @@ -122,18 +130,26 @@ class MailActivity(models.Model): if record.previous_activity_type_id.triggered_next_type_id: record.activity_type_id = record.previous_activity_type_id.triggered_next_type_id + @api.depends('active') + def _compute_date_done(self): + unarchived = self.filtered('active') + unarchived.date_done = False + # keep earliest archive date if multi archive + toupdate = (self - unarchived).filtered(lambda act: not act.date_done) + toupdate.date_done = fields.Datetime.now() + @api.depends('res_model', 'res_id') def _compute_res_name(self): for activity in self: activity.res_name = activity.res_model and \ self.env[activity.res_model].browse(activity.res_id).display_name - @api.depends('date_deadline') + @api.depends('active', 'date_deadline') def _compute_state(self): for record in self.filtered(lambda activity: activity.date_deadline): tz = record.user_id.sudo().tz date_deadline = record.date_deadline - record.state = self._compute_state_from_date(date_deadline, tz) + record.state = 'done' if not record.active else self._compute_state_from_date(date_deadline, tz) @api.model def _compute_state_from_date(self, date_deadline, tz=False): @@ -533,6 +549,10 @@ class MailActivity(models.Model): mail_activity_type_id=activity.activity_type_id.id, subtype_xmlid='mail.mt_activities', ) + if activity.activity_type_id.keep_done: + attachment_ids = (attachment_ids or []) + activity_attachments.get(activity.id, []) + if attachment_ids: + activity.attachment_ids = attachment_ids # Moving the attachments in the message # TODO: Fix void res_id on attachment when you create an activity with an image @@ -551,7 +571,9 @@ class MailActivity(models.Model): if next_activities_values: next_activities = self.env['mail.activity'].create(next_activities_values) - self.unlink() # will unlink activity, dont access `self` after that + activity_to_keep = self.filtered('activity_type_id.keep_done') + activity_to_keep.action_archive() + (self - activity_to_keep).unlink() # will unlink activity, dont access `self` after that return messages, next_activities @@ -572,53 +594,152 @@ class MailActivity(models.Model): def activity_format(self): activities = self.read() self.mail_template_ids.fetch(['name']) + self.attachment_ids.fetch(['name']) for record, activity in zip(self, activities): activity['mail_template_ids'] = [ {'id': mail_template.id, 'name': mail_template.name} for mail_template in record.mail_template_ids ] + activity['attachment_ids'] = [ + {'id': attachment.id, 'name': attachment.name} + for attachment in record.attachment_ids + ] return activities @api.model - def get_activity_data(self, res_model, domain, limit=None, offset=0): - activity_domain = [('res_model', '=', res_model)] - if domain or limit or offset: - res = self.env[res_model].search(domain or [], limit=limit, offset=offset) - activity_domain.append(('res_id', 'in', res.ids)) - grouped_activities = self.env['mail.activity']._read_group( - activity_domain, - ['res_id', 'activity_type_id'], - ['id:array_agg', 'date_deadline:min', '__count']) - # filter out unreadable records - if not domain: - res_ids = tuple(res_id for res_id, *_ in grouped_activities) - res_ids_set = set(self.env[res_model].search([('id', 'in', res_ids)])._ids) - grouped_activities = [a for a in grouped_activities if a[0] in res_ids_set] - res_id_to_deadline = {} - activity_data = defaultdict(dict) - for res_id, activity_type, ids, date_deadline, count in grouped_activities: - activity_type_id = activity_type.id - res_id_to_deadline[res_id] = date_deadline if (res_id not in res_id_to_deadline or date_deadline < res_id_to_deadline[res_id]) else res_id_to_deadline[res_id] - state = self._compute_state_from_date(date_deadline, self.user_id.sudo().tz) - activity_data[res_id][activity_type_id] = { - 'count': count, - 'ids': ids, - 'state': state, - 'o_closest_deadline': date_deadline, - } - activity_type_infos = [] - activity_type_ids = self.env['mail.activity.type'].search( - ['|', ('res_model', '=', res_model), ('res_model', '=', False)]) - for elem in sorted(activity_type_ids, key=lambda item: item.sequence): - mail_template_info = [] - for mail_template_id in elem.mail_template_ids: - mail_template_info.append({"id": mail_template_id.id, "name": mail_template_id.name}) - activity_type_infos.append([elem.id, elem.name, mail_template_info]) + def get_activity_data(self, res_model, domain, limit=None, offset=0, fetch_done=False): + """ Get aggregate data about records and their activities. + The goal is to fetch and compute aggregated data about records and their + activities to display them in the activity views and the chatter. For example, + the activity view displays it as a table with columns and rows being respectively + the activity_types and the activity_res_ids, and the grouped_activities being the + table entries with the aggregated data. + + :param str res_model: model of the records to fetch + :param list domain: record search domain + :param int limit: maximum number of records to fetch + :param int offset: offset of the first record to fetch + :param bool fetch_done: determines if "done" activities are integrated in the + aggregated data or not. + :return dict: {'activity_types': dict of activity type info + {id: int, name: str, mail_template: list of {id:int, name:str}, + keep_done: bool} + 'activity_res_ids': list of record id ordered by closest date + (deadline for ongoing activities, and done date for done activities) + 'grouped_activities': dict + res_id -> activity_type_id -> aggregated info as: + count_by_state dict: mapping state to count (ex.: 'planned': 2) + ids list: activity ids for the res_id and activity_type_id + reporting_date str: aggregated date of the related activities as + oldest deadline of ongoing activities if there are any + or most recent date done of completed activities + state dict: aggregated state of the related activities + user_assigned_ids list: activity responsible id ordered + by closest deadline of the related activities + attachments_info: dict with information about the attachments + {'count': int, 'most_recent_id': int, 'most_recent_name': str} + } + """ + user_tz = self.user_id.sudo().tz + DocModel = self.env[res_model] + Activity = self.env['mail.activity'] + + # 1. Retrieve all ongoing and completed activities according to the parameters + activity_types = self.env['mail.activity.type'].search([('res_model', 'in', (res_model, False))]) + fetch_done = fetch_done and activity_types.filtered('keep_done') + activity_domain = [('res_model', '=', res_model)] + is_filtered = domain or limit or offset + if is_filtered: + activity_domain.append(('res_id', 'in', DocModel._search(domain or [], offset, limit) if is_filtered else [])) + all_activities = Activity.with_context(active_test=not fetch_done).search( + activity_domain, order='date_done DESC, date_deadline ASC') + all_ongoing = all_activities.filtered('active') + all_completed = all_activities.filtered(lambda act: not act.active) + + # 2. Get attachment of completed activities + if all_completed: + attachment_ids = all_completed.attachment_ids.ids + attachments_by_id = { + a['id']: a + for a in self.env['ir.attachment'].search_read([['id', 'in', attachment_ids]], ['create_date', 'name']) + } if attachment_ids else {} + else: + attachments_by_id = {} + + # 3. Group activities per records and activity type + grouped_completed = {group: Activity.browse([v.id for v in values]) + for group, values in groupby(all_completed, key=lambda a: (a.res_id, a.activity_type_id))} + grouped_ongoing = {group: Activity.browse([v.id for v in values]) + for group, values in groupby(all_ongoing, key=lambda a: (a.res_id, a.activity_type_id))} + + # 4. Filter out unreadable records + res_id_type_tuples = grouped_ongoing.keys() | grouped_completed.keys() + if not is_filtered: + filtered = set(DocModel.search([('id', 'in', [r[0] for r in res_id_type_tuples])]).ids) + res_id_type_tuples = list(filter(lambda r: r[0] in filtered, res_id_type_tuples)) + + # 5. Format data + res_id_to_date_done = {} + res_id_to_deadline = {} + grouped_activities = defaultdict(dict) + for res_id_tuple in res_id_type_tuples: + res_id, activity_type_id = res_id_tuple + ongoing = grouped_ongoing.get(res_id_tuple, Activity) + completed = grouped_completed.get(res_id_tuple, Activity) + activities = ongoing | completed + + # As completed is sorted on date_done DESC, we take here the max date_done + date_done = completed and completed[0].date_done + # As ongoing is sorted on date_deadline ASC, we take here the min date_deadline + date_deadline = ongoing and ongoing[0].date_deadline + if date_deadline and (res_id not in res_id_to_deadline or date_deadline < res_id_to_deadline[res_id]): + res_id_to_deadline[res_id] = date_deadline + if date_done and (res_id not in res_id_to_date_done or date_done > res_id_to_date_done[res_id]): + res_id_to_date_done[res_id] = date_done + # As ongoing is sorted on date_deadline, we get assignees on activity with oldest deadline first + user_assigned_ids = ongoing.user_id.ids + attachments = [attachments_by_id[attach.id] for attach in completed.attachment_ids] + grouped_activities[res_id][activity_type_id.id] = { + 'count_by_state': dict(Counter( + self._compute_state_from_date(act.date_deadline, user_tz) if act.active else 'done' + for act in activities)), + 'ids': activities.ids, + 'reporting_date': ongoing and date_deadline or date_done or None, + 'state': self._compute_state_from_date(date_deadline, user_tz) if ongoing else 'done', + 'user_assigned_ids': user_assigned_ids, + } + if attachments: + most_recent_attachment = max(attachments, key=lambda a: (a['create_date'], a['id'])) + grouped_activities[res_id][activity_type_id.id]['attachments_info'] = { + 'most_recent_id': most_recent_attachment['id'], + 'most_recent_name': most_recent_attachment['name'], + 'count': len(attachments), + } + + # Get record ids ordered by oldest deadline (urgent one first) + ongoing_res_ids = sorted(res_id_to_deadline, key=lambda item: res_id_to_deadline[item]) + # Get record ids with only completed activities ordered by date done reversed (most recently done first) + completed_res_ids = [ + res_id for res_id in sorted( + res_id_to_date_done, key=lambda item: res_id_to_date_done[item], reverse=True + ) if res_id not in res_id_to_deadline + ] return { - 'activity_types': activity_type_infos, - 'activity_res_ids': sorted(res_id_to_deadline, key=lambda item: res_id_to_deadline[item]), - 'grouped_activities': activity_data, + 'activity_res_ids': ongoing_res_ids + completed_res_ids, + 'activity_types': [ + { + 'id': activity_type.id, + 'keep_done': activity_type.keep_done, + 'name': activity_type.name, + 'template_ids': [ + {'id': mail_template_id.id, 'name': mail_template_id.name} + for mail_template_id in activity_type.mail_template_ids + ], + } + for activity_type in activity_types + ], + 'grouped_activities': grouped_activities, } # ---------------------------------------------------------------------- diff --git a/addons/mail/models/mail_activity_type.py b/addons/mail/models/mail_activity_type.py index 83dedf74bbd..91a33204ec1 100644 --- a/addons/mail/models/mail_activity_type.py +++ b/addons/mail/models/mail_activity_type.py @@ -71,6 +71,7 @@ class MailActivityType(models.Model): mail_template_ids = fields.Many2many('mail.template', string='Email templates') default_user_id = fields.Many2one("res.users", string="Default User") default_note = fields.Html(string="Default Note", translate=True) + keep_done = fields.Boolean(string="Keep Done", help='Keep activities marked as done in the activity view') #Fields for display purpose only initial_res_model = fields.Selection(selection=_get_model_selection, string='Initial model', compute="_compute_initial_res_model", store=False, diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 5ace9f997da..27b0d11badd 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -4183,7 +4183,7 @@ class MailThread(models.AbstractModel): except AccessError: pass if 'activities' in request_list: - res['activities'] = self.activity_ids.activity_format() + res['activities'] = self.with_context(active_test=True).activity_ids.activity_format() if 'attachments' in request_list: res['attachments'] = self._get_mail_thread_data_attachments()._attachment_format() if 'followers' in request_list: diff --git a/addons/mail/static/src/core/web/activity_list_popover.js b/addons/mail/static/src/core/web/activity_list_popover.js index de77115a607..e4cc764a722 100644 --- a/addons/mail/static/src/core/web/activity_list_popover.js +++ b/addons/mail/static/src/core/web/activity_list_popover.js @@ -62,6 +62,10 @@ export class ActivityListPopover extends Component { this.props.close(); } + get doneActivities() { + return this.activities.filter((activity) => activity.state === "done"); + } + get overdueActivities() { return this.activities.filter((activity) => activity.state === "overdue"); } diff --git a/addons/mail/static/src/core/web/activity_list_popover.xml b/addons/mail/static/src/core/web/activity_list_popover.xml index 8bf55bfa4c5..ba714911fe3 100644 --- a/addons/mail/static/src/core/web/activity_list_popover.xml +++ b/addons/mail/static/src/core/web/activity_list_popover.xml @@ -28,6 +28,14 @@ + +
+ Done + + +
+ +
- 0 Late - - 0 Today + + Late + + + Today + - - 0 Future + + Future + - + diff --git a/addons/mass_mailing/models/mailing.py b/addons/mass_mailing/models/mailing.py index 938d493b880..dbe302f8bd7 100644 --- a/addons/mass_mailing/models/mailing.py +++ b/addons/mass_mailing/models/mailing.py @@ -46,6 +46,7 @@ class MassMailing(models.Model): ] _order = 'calendar_date DESC' _rec_name = "subject" + _systray_view = 'list' @api.model def default_get(self, fields_list): diff --git a/addons/mass_mailing_sms/models/res_users.py b/addons/mass_mailing_sms/models/res_users.py index 8e5a4eb730a..d6f569bbd98 100644 --- a/addons/mass_mailing_sms/models/res_users.py +++ b/addons/mass_mailing_sms/models/res_users.py @@ -17,6 +17,7 @@ class Users(models.Model): doing a new query to split them by mailing_type. """ activities = super(Users, self).systray_get_activities() + view_type = self.env['mailing.mailing']._systray_view for activity in activities: if activity.get('model') == 'mailing.mailing': activities.remove(activity) @@ -56,6 +57,7 @@ class Users(models.Model): 'icon': icon, 'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0, 'res_ids': res_ids, + "view_type": view_type, } user_activities[act['mailing_type']]['res_ids'].add(act['res_id']) user_activities[act['mailing_type']]['%s_count' % act['states']] += act['count'] @@ -64,7 +66,6 @@ class Users(models.Model): for mailing_type in user_activities.keys(): user_activities[mailing_type].update({ - 'actions': [{'icon': 'fa-clock-o', 'name': 'Summary',}], 'domain': json.dumps([['activity_ids.res_id', 'in', list(user_activities[mailing_type]['res_ids'])]]) }) activities.extend(list(user_activities.values())) diff --git a/addons/mass_mailing_sms/static/tests/tours/mailing_activities_split.js b/addons/mass_mailing_sms/static/tests/tours/mailing_activities_split.js index 43299702823..cd8c2d2da7a 100644 --- a/addons/mass_mailing_sms/static/tests/tours/mailing_activities_split.js +++ b/addons/mass_mailing_sms/static/tests/tours/mailing_activities_split.js @@ -14,9 +14,9 @@ registry.category("web_tour.tours").add('mailing_activities_split', { trigger: '.o-mail-ActivityGroup:contains("Email Marketing")', }, { content: 'Open Email Marketing record in the kanban view', - trigger: '.o_kanban_record:contains("New Email!")', + trigger: '.o_list_renderer .o_data_cell:contains("New Email!")', run: () => { - if ($(".o_kanban_record:contains(New SMS!)").length !== 0) { + if ($('.o_list_renderer .o_data_cell:contains("New SMS!")').length !== 0) { console.error('SMS Marketing record should not appear in this view'); } }, @@ -28,9 +28,9 @@ registry.category("web_tour.tours").add('mailing_activities_split', { trigger: '.o-mail-ActivityGroup:contains("SMS Marketing")', }, { content: 'Open SMS Marketing record in the kanban view', - trigger: '.o_kanban_record:contains("New SMS!")', + trigger: '.o_list_renderer .o_data_cell:contains("New SMS!")', run: () => { - if ($(".o_kanban_record:contains(New Email!)").length !== 0) { + if ($('.o_list_renderer .o_data_cell:contains("New Email!")').length !== 0) { console.error('Email Marketing record should not appear in this view'); } }, diff --git a/addons/project/models/project_project.py b/addons/project/models/project_project.py index 42e0d21373c..5d2b15366c6 100644 --- a/addons/project/models/project_project.py +++ b/addons/project/models/project_project.py @@ -19,6 +19,7 @@ class Project(models.Model): _inherit = ['portal.mixin', 'mail.alias.mixin', 'rating.parent.mixin', 'mail.thread', 'mail.activity.mixin'] _order = "sequence, name, id" _rating_satisfaction_days = 30 # takes 30 days by default + _systray_view = 'activity' def _compute_attached_docs_count(self): self.env.cr.execute( diff --git a/addons/project/models/project_task.py b/addons/project/models/project_task.py index 3166c3b351a..e381bb7879b 100644 --- a/addons/project/models/project_task.py +++ b/addons/project/models/project_task.py @@ -77,6 +77,7 @@ class Task(models.Model): _mail_post_access = 'read' _order = "priority desc, sequence, date_deadline asc, id desc" _primary_email = 'email_from' + _systray_view = 'activity' _track_duration_field = 'stage_id' @api.model diff --git a/addons/project_todo/static/tests/activity_menu_tests.js b/addons/project_todo/static/tests/activity_menu_tests.js index 3ba0469e7b6..4ff71e9e9ae 100644 --- a/addons/project_todo/static/tests/activity_menu_tests.js +++ b/addons/project_todo/static/tests/activity_menu_tests.js @@ -27,8 +27,8 @@ QUnit.test("create todo from activity menu without date", async function () { // Need to reopen systray as it automatically closes when a todo is created await click(".o_menu_systray i[aria-label='Activities']"); await contains(".o-mail-ActivityMenu-counter", { text: "1" }); - await contains(".o-mail-ActivityGroup", { text: "project.task" }); - await contains(".btn", { text: "1 Today" }); + await contains(".o-mail-ActivityGroup span", { text: "project.task" }); + await contains(".span", { text: "1 Today" }); await contains(".btn", { text: "Add a To-do" }); await contains(".o-mail-ActivityMenu-show", { count: 0 }); await contains(".o-mail-ActivityMenu-input", { count: 0 }); @@ -44,7 +44,7 @@ QUnit.test("create todo from activity menu without date", async function () { }); await click(".o_menu_systray i[aria-label='Activities']"); await contains(".o-mail-ActivityMenu-counter", { text: "2" }); - await contains(".btn", { text: "2 Today" }); + await contains(".span", { text: "2 Today" }); }); QUnit.test("create to-do from activity menu with date", async function () { @@ -72,6 +72,6 @@ QUnit.test("create to-do from activity menu with date", async function () { // Need to reopen systray as it automatically closes when a todo is created await click(".o_menu_systray i[aria-label='Activities']"); await contains(".o-mail-ActivityMenu-counter", { text: "1" }); - await contains(".o-mail-ActivityGroup", { text: "project.task" }); - await contains(".btn", { text: "1 Future" }); + await contains(".o-mail-ActivityGroup span", { text: "project.task" }); + await contains(".span", { text: "1 Future" }); }); diff --git a/addons/test_mail/static/tests/systray_activity_menu_tests.js b/addons/test_mail/static/tests/systray_activity_menu_tests.js index fa398e250af..1819bedcac0 100644 --- a/addons/test_mail/static/tests/systray_activity_menu_tests.js +++ b/addons/test_mail/static/tests/systray_activity_menu_tests.js @@ -45,6 +45,12 @@ QUnit.test("do not show empty text when at least some future activities", async }); QUnit.test("activity menu widget: activity menu with 2 models", async (assert) => { + function selectContaining(domElement, selector, containings) { + return Array.from(domElement.querySelectorAll(selector)).filter((sel) => + containings.every((containing) => sel.textContent.includes(containing)) + ); + } + const tomorrow = today().plus({ days: 1 }); const yesterday = today().plus({ days: -1 }); const pyEnv = await startServer(); @@ -73,102 +79,46 @@ QUnit.test("activity menu widget: activity menu with 2 models", async (assert) = await contains(".o_menu_systray i[aria-label='Activities']"); await contains(".o-mail-ActivityMenu-counter"); await contains(".o-mail-ActivityMenu-counter", { text: "5" }); - let context = {}; + const actionChecks = { + context: { force_search_count: 1 }, + domain: [["activity_ids.user_id", "=", session.uid]], + }; patchWithCleanup(env.services.action, { doAction(action) { - assert.deepEqual(action.context, context); + Object.entries(actionChecks).forEach(([key, value]) => { + assert.deepEqual(action[key], value); + }); + assert.step("do_action:" + action.name); }, }); - context = { - force_search_count: 1, - search_default_activities_overdue: 1, - }; await click(".o_menu_systray i[aria-label='Activities']"); await contains(".o-mail-ActivityMenu"); await contains(".o-mail-ActivityMenu .o-mail-ActivityGroup", { count: 2 }); - await click(".o-mail-ActivityMenu .o-mail-ActivityGroup button", { text: "1 Late" }); + assert.ok( + selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["res.partner", "0 Late"]) + ); + assert.ok( + selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["res.partner", "1 Today"]) + ); + assert.ok( + selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["res.partner", "0 Future"]) + ); + assert.ok( + selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["mail.test.activity", "1 Late"]) + ); + assert.ok( + selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["mail.test.activity", "1 Today"]) + ); + assert.ok( + selectContaining(document, ".o-mail-ActivityMenu .o-mail-ActivityGroup", ["mail.test.activity", "2 Future"]) + ); + actionChecks.res_model = "res.partner"; + await click(".o-mail-ActivityMenu .o-mail-ActivityGroup", { text: "res.partner" }); await contains(".o-mail-ActivityMenu", { count: 0 }); - context = { - force_search_count: 1, - search_default_activities_today: 1, - }; - await click(".o_menu_systray .dropdown-toggle:has(i[aria-label='Activities'])"); - await click(":nth-child(1 of .o-mail-ActivityGroup) button", { text: "1 Today" }); - await contains(".o-mail-ActivityMenu", { count: 0 }); - context = { - force_search_count: 1, - search_default_activities_upcoming_all: 1, - }; - await click(".o_menu_systray i[aria-label='Activities']"); - await click(".o-mail-ActivityMenu .o-mail-ActivityGroup button", { text: "2 Future" }); - await contains(".o-mail-ActivityMenu", { count: 0 }); - context = { - force_search_count: 1, - search_default_activities_overdue: 1, - search_default_activities_today: 1, - }; await click(".o_menu_systray i[aria-label='Activities']"); + actionChecks.res_model = "mail.test.activity"; await click(".o-mail-ActivityMenu .o-mail-ActivityGroup", { text: "mail.test.activity" }); - await contains(".o-mail-ActivityMenu", { count: 0 }); -}); - -QUnit.test("activity menu widget: activity view icon", async (assert) => { - const tomorrow = today().plus({ days: 1 }); - const yesterday = today().plus({ days: -1 }); - const pyEnv = await startServer(); - const partnerId = pyEnv["res.partner"].create({}); - const activityIds = pyEnv["mail.test.activity"].create([{}, {}, {}, {}]); - pyEnv["mail.activity"].create([ - { res_id: partnerId, res_model: "res.partner" }, - { res_id: activityIds[0], res_model: "mail.test.activity" }, - { - date_deadline: serializeDate(tomorrow), - res_id: activityIds[1], - res_model: "mail.test.activity", - }, - { - date_deadline: serializeDate(tomorrow), - res_id: activityIds[2], - res_model: "mail.test.activity", - }, - { - date_deadline: serializeDate(yesterday), - res_id: activityIds[3], - res_model: "mail.test.activity", - }, - ]); - const { env } = await start(); - await click(".o_menu_systray i[aria-label='Activities']"); - await contains("button[title='Summary']", { count: 2 }); - await contains(".o-mail-ActivityGroup", { - text: "res.partner", - contains: ["button[title='Summary'].fa-clock-o"], - }); - await contains(".o-mail-ActivityGroup", { - text: "mail.test.activity", - contains: ["button[title='Summary'].fa-clock-o"], - }); - await contains(".show .dropdown-menu"); - patchWithCleanup(env.services.action, { - doAction(action) { - if (action.name) { - assert.ok(action.domain); - assert.deepEqual(action.domain, [["activity_ids.user_id", "=", session.uid]]); - assert.step("do_action:" + action.name); - } else { - assert.step("do_action:" + action); - } - }, - }); - await click("button[title='Summary']", { - parent: [".o-mail-ActivityGroup", { text: "mail.test.activity" }], - }); - await contains(".dropdown-menu", { count: 0 }); - await click(".o_menu_systray i[aria-label='Activities']"); - await click("button[title='Summary']", { - parent: [".o-mail-ActivityGroup", { text: "res.partner" }], - }); - assert.verifySteps(["do_action:mail.test.activity", "do_action:res.partner"]); + assert.verifySteps(["do_action:res.partner", "do_action:mail.test.activity"]); }); QUnit.test("activity menu widget: close on messaging menu click", async () => { diff --git a/addons/test_mail/tests/test_mail_multicompany.py b/addons/test_mail/tests/test_mail_multicompany.py index 32702e2f0fd..1c05575c648 100644 --- a/addons/test_mail/tests/test_mail_multicompany.py +++ b/addons/test_mail/tests/test_mail_multicompany.py @@ -271,7 +271,6 @@ class TestMultiCompanySetup(TestMailMCCommon): self.assertEqual( test_activity, { - "actions": [{"icon": "fa-clock-o", "name": "Summary"}], "icon": "/base/static/description/icon.png", "id": self.env["ir.model"]._get_id("mail.test.multi.company.with.activity"), "model": "mail.test.multi.company.with.activity", @@ -281,6 +280,7 @@ class TestMultiCompanySetup(TestMailMCCommon): "today_count": 2, "total_count": 2, "type": "activity", + "view_type": "list", } ) @@ -291,7 +291,6 @@ class TestMultiCompanySetup(TestMailMCCommon): self.assertEqual( test_activity, { - "actions": [{"icon": "fa-clock-o", "name": "Summary"}], "icon": "/base/static/description/icon.png", "id": self.env["ir.model"]._get_id("mail.test.multi.company.with.activity"), "model": "mail.test.multi.company.with.activity", @@ -301,6 +300,7 @@ class TestMultiCompanySetup(TestMailMCCommon): "today_count": 1, "total_count": 1, "type": "activity", + "view_type": "list", } ) From 723b0ff5d5a530a61826eab4c4b93acfde0ddc9c Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Wed, 21 Jun 2023 10:33:36 +0200 Subject: [PATCH 3/6] [IMP] various: add activity view for different model We add the activity view for: - event.event - maintenance.request - mrp.production, mrp.unbuild - repair.order - stock.picking Task-3300854 Part-of: odoo/odoo#138135 --- addons/event/views/event_event_views.xml | 2 +- addons/maintenance/views/maintenance_views.xml | 12 ++++++------ addons/mrp/views/mrp_production_views.xml | 2 +- addons/mrp/views/mrp_unbuild_views.xml | 2 +- addons/mrp/views/stock_picking_views.xml | 2 +- addons/repair/views/repair_views.xml | 2 +- addons/stock/views/stock_picking_views.xml | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/addons/event/views/event_event_views.xml b/addons/event/views/event_event_views.xml index 84a250aef8d..1a8d0a2b45f 100644 --- a/addons/event/views/event_event_views.xml +++ b/addons/event/views/event_event_views.xml @@ -295,7 +295,7 @@ Events event.event - kanban,calendar,tree,form,pivot,graph + kanban,calendar,tree,form,pivot,graph,activity

diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml index 399398a7faa..8ebb94a64e7 100644 --- a/addons/maintenance/views/maintenance_views.xml +++ b/addons/maintenance/views/maintenance_views.xml @@ -264,7 +264,7 @@ Maintenance Requests maintenance.request - kanban,tree,form,pivot,graph,calendar + kanban,tree,form,pivot,graph,calendar,activity { 'search_default_active': True, @@ -282,7 +282,7 @@ Maintenance Requests maintenance.request - kanban,tree,form,pivot,graph,calendar + kanban,tree,form,pivot,graph,calendar,activity { @@ -303,7 +303,7 @@ Maintenance Requests maintenance.request - kanban,tree,form,pivot,graph,calendar + kanban,tree,form,pivot,graph,calendar,activity { 'search_default_active': True, 'default_equipment_id': active_id, @@ -321,7 +321,7 @@ Maintenance Requests maintenance.request - kanban,tree,form,pivot,graph,calendar + kanban,tree,form,pivot,graph,calendar,activity { 'search_default_active': True, 'search_default_maintenance_team_id': active_id, @@ -340,7 +340,7 @@ Maintenance Requests maintenance.request - calendar,kanban,tree,form,pivot,graph + calendar,kanban,tree,form,pivot,graph,activity { 'search_default_active': True, @@ -358,7 +358,7 @@ Maintenance Requests maintenance.request - graph,pivot,kanban,tree,form,calendar + graph,pivot,kanban,tree,form,calendar,activity { 'search_default_active': True, } diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 207c412fefd..de8769366c1 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -616,7 +616,7 @@ Manufacturing Orders mrp.production - tree,kanban,form,calendar,pivot,graph + tree,kanban,form,calendar,pivot,graph,activity {'search_default_todo': True, 'default_company_id': allowed_company_ids[0]} diff --git a/addons/mrp/views/mrp_unbuild_views.xml b/addons/mrp/views/mrp_unbuild_views.xml index bd88f70df1d..7887ae849bb 100644 --- a/addons/mrp/views/mrp_unbuild_views.xml +++ b/addons/mrp/views/mrp_unbuild_views.xml @@ -196,7 +196,7 @@ Unbuild Orders mrp.unbuild - tree,kanban,form + tree,kanban,form,activity

No unbuild order found diff --git a/addons/mrp/views/stock_picking_views.xml b/addons/mrp/views/stock_picking_views.xml index b27e0b8a257..cdaf45c8130 100644 --- a/addons/mrp/views/stock_picking_views.xml +++ b/addons/mrp/views/stock_picking_views.xml @@ -144,7 +144,7 @@ Manufacturings mrp.production - tree,kanban,form,calendar + tree,kanban,form,calendar,activity {'default_company_id': allowed_company_ids[0]} diff --git a/addons/repair/views/repair_views.xml b/addons/repair/views/repair_views.xml index 20a71f39a6a..3d954ce0419 100644 --- a/addons/repair/views/repair_views.xml +++ b/addons/repair/views/repair_views.xml @@ -298,7 +298,7 @@ Repair Orders repair.order - tree,kanban,graph,pivot,form + tree,kanban,graph,pivot,form,activity

diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index 16ad4d8790c..5de4d89d4e7 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -436,7 +436,7 @@ Receipts stock.picking - tree,kanban,form,calendar + tree,kanban,form,calendar,activity {'contact_display': 'partner_address', 'default_company_id': allowed_company_ids[0], 'restricted_picking_type_code': 'incoming', 'search_default_reception': 1} @@ -452,7 +452,7 @@ Deliveries stock.picking - tree,kanban,form,calendar + tree,kanban,form,calendar,activity {'contact_display': 'partner_address', 'default_company_id': allowed_company_ids[0], 'restricted_picking_type_code': 'outgoing', 'search_default_delivery': 1} From 725e7ffae359b11b18c41a65791b9fded1d21fc5 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Tue, 27 Jun 2023 12:12:14 +0200 Subject: [PATCH 4/6] [IMP] various: improve custom activity views The following summary view of the record in the activity views have been improved: - project_task: task state added - project_project: project manager added - event_event: responsible and the dates (date_begin_located and date_end_located) added - account_move: total amount, customer and state added - sale_order: total amount and the state added - purchase_order: total amount and the state added - crm_lead: customer and stage added - hr_applicant (hr_recruitment): recruiter added - survey_survey: responsible added - maintenance.request: added equipment and responsible - stock.picking: added scheduled_date - repair.order: responsible, schedule_date and product_id added - mrp.production: added responsible - hr_leave (hr_holidays): status is added and default deadline modified see below Ensures that "Schedule activity" is in one line by adding a colspan. hr_leave activity: When a time off approval activity is created as a consequence of the creation of a hr.leave, the deadline of the activity is set to the date_from of the hr.leave minus activity type delay_count (default 15) except if it leads to a date anterior to today. In that case it is set to today. That way, in the activity view, the time off approval activities are more or less sorted by related hr.leave date_from and the cell date is an indication of when the time off is planned. Technical note: on most activity view, the activity record was not occupying all the horizontal space. To solve that problem the css has been modified and the max-width (200) that was imposed on the sub div has been removed. And as it was impossible to impose a max-width for a flex div (which is the common case for the activity record), the max width is imposed on each text that might be too long using the class o_text_block. That class has been modified to impose a max-width. That's why that class has been added in most view. Alternativly, we could have modified the activity compiler to add that class when the attribute full was set (not done because not sure of the consequence). Task-3300854 Part-of: odoo/odoo#138135 --- addons/account/views/account_move_views.xml | 24 +++++++++++++++++++ addons/crm/views/crm_lead_views.xml | 14 ++++++++--- addons/event/views/event_event_views.xml | 20 ++++++++++++++++ .../fleet/views/fleet_vehicle_cost_views.xml | 6 ++--- addons/fleet/views/fleet_vehicle_views.xml | 4 ++-- addons/hr/views/hr_employee_views.xml | 4 ++-- .../hr_contract/views/hr_contract_views.xml | 4 ++-- addons/hr_expense/views/hr_expense_views.xml | 4 ++-- .../data/mail_activity_type_data.xml | 1 + addons/hr_holidays/models/hr_leave.py | 12 +++++++++- .../views/hr_leave_allocation_views.xml | 2 +- addons/hr_holidays/views/hr_leave_views.xml | 13 +++++----- .../views/hr_applicant_views.xml | 7 +++--- .../web/activity/activity_controller.scss | 12 ++-------- .../views/web/activity/activity_renderer.xml | 2 +- addons/mail/views/res_partner_views.xml | 4 ++-- .../maintenance/views/maintenance_views.xml | 21 ++++++++++++++++ addons/mrp/views/mrp_production_views.xml | 18 ++++++++++++++ .../product/views/product_template_views.xml | 4 ++-- addons/product/views/product_views.xml | 4 ++-- .../project_task_state_selection.js | 4 ++-- .../project_task_state_selection.xml | 10 ++++---- addons/project/static/src/css/project.css | 4 ++++ .../project/views/project_project_views.xml | 15 ++++++++++++ addons/project/views/project_task_views.xml | 22 +++++++++-------- .../project_todo/views/project_task_views.xml | 2 +- addons/purchase/views/purchase_views.xml | 18 ++++++++++---- addons/repair/views/repair_views.xml | 19 +++++++++++++++ addons/sale/views/sale_order_views.xml | 18 ++++++++++---- addons/stock/views/stock_picking_views.xml | 15 ++++++++++++ addons/survey/views/survey_survey_views.xml | 18 ++++++++++++++ 31 files changed, 256 insertions(+), 69 deletions(-) diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index f58f47797a4..e2400c3cf88 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -1286,6 +1286,30 @@ + + account.move.view.activity + account.move + + + + +

+
+ +
+ +
+
+ +
+ +
+
+ + +
+ + account.move.select account.move diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index bca2e13778b..6dbd85964d1 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -513,9 +513,17 @@
-
- - +
+
+ +
+ +
+
+ +
+ +
diff --git a/addons/event/views/event_event_views.xml b/addons/event/views/event_event_views.xml index 1a8d0a2b45f..332bd852f72 100644 --- a/addons/event/views/event_event_views.xml +++ b/addons/event/views/event_event_views.xml @@ -158,6 +158,26 @@ + + event.event.view.activity + event.event + + + +
+ +
+ + + + +
+
+
+
+
+
+ event.event.form.quick_create event.event diff --git a/addons/fleet/views/fleet_vehicle_cost_views.xml b/addons/fleet/views/fleet_vehicle_cost_views.xml index 056c1e382b9..7c33302188c 100644 --- a/addons/fleet/views/fleet_vehicle_cost_views.xml +++ b/addons/fleet/views/fleet_vehicle_cost_views.xml @@ -179,7 +179,7 @@
- +
@@ -356,8 +356,8 @@
- - + +
diff --git a/addons/fleet/views/fleet_vehicle_views.xml b/addons/fleet/views/fleet_vehicle_views.xml index 84d94c60811..a736ea658d7 100644 --- a/addons/fleet/views/fleet_vehicle_views.xml +++ b/addons/fleet/views/fleet_vehicle_views.xml @@ -338,8 +338,8 @@
- - + +
diff --git a/addons/hr/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml index 7a61d658452..4978fd1a009 100644 --- a/addons/hr/views/hr_employee_views.xml +++ b/addons/hr/views/hr_employee_views.xml @@ -331,8 +331,8 @@
- - + +
diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml index dc09d32485a..9ee2d1cdcd9 100644 --- a/addons/hr_contract/views/hr_contract_views.xml +++ b/addons/hr_contract/views/hr_contract_views.xml @@ -355,8 +355,8 @@
- - + +
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index b6e841338b4..f8747a39284 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -396,7 +396,7 @@
- +
@@ -1025,7 +1025,7 @@
- +
diff --git a/addons/hr_holidays/data/mail_activity_type_data.xml b/addons/hr_holidays/data/mail_activity_type_data.xml index 19858334853..89a5d483f35 100644 --- a/addons/hr_holidays/data/mail_activity_type_data.xml +++ b/addons/hr_holidays/data/mail_activity_type_data.xml @@ -6,6 +6,7 @@ Time Off Approval fa-sun-o hr.leave + 15 Time Off Second Approve diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index a525720fa84..a62173d7dd8 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -9,6 +9,7 @@ import pytz from collections import namedtuple, defaultdict from datetime import datetime, timedelta, time +from dateutil.relativedelta import relativedelta from pytz import timezone, UTC from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG @@ -1460,10 +1461,19 @@ class HolidaysRequest(models.Model): elif holiday.state == 'confirm': if holiday.holiday_status_id.leave_validation_type != 'no_validation': user_ids = holiday.sudo()._get_responsible_for_approval().ids or self.env.user.ids + today = fields.Date.today() + activity_type = self.env.ref('hr_holidays.mail_act_leave_approval') for user_id in user_ids: + date_deadline = ( + (holiday.date_from - + relativedelta(**{activity_type.delay_unit: activity_type.delay_count or 0})).date() + if holiday.date_from else today) + if date_deadline < today: + date_deadline = today activity_vals.append({ - 'activity_type_id': self.env.ref('hr_holidays.mail_act_leave_approval').id, + 'activity_type_id': activity_type.id, 'automated': True, + 'date_deadline': date_deadline, 'note': note, 'user_id': user_id, 'res_id': holiday.id, diff --git a/addons/hr_holidays/views/hr_leave_allocation_views.xml b/addons/hr_holidays/views/hr_leave_allocation_views.xml index 38853ca7e34..34ddcdbcc3b 100644 --- a/addons/hr_holidays/views/hr_leave_allocation_views.xml +++ b/addons/hr_holidays/views/hr_leave_allocation_views.xml @@ -431,7 +431,7 @@
- ( days) + ( days)
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml index 4398b0169dd..88d7bab74c2 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -184,15 +184,16 @@ - - -
- -
- ( days) + +
+
+ +
( days)
+
+
diff --git a/addons/hr_recruitment/views/hr_applicant_views.xml b/addons/hr_recruitment/views/hr_applicant_views.xml index d38b90fa788..c871581e0b6 100644 --- a/addons/hr_recruitment/views/hr_applicant_views.xml +++ b/addons/hr_recruitment/views/hr_applicant_views.xml @@ -407,9 +407,10 @@
-
- - + +
+ +
diff --git a/addons/mail/static/src/views/web/activity/activity_controller.scss b/addons/mail/static/src/views/web/activity/activity_controller.scss index 60efe2bc746..b75cf15dca0 100644 --- a/addons/mail/static/src/views/web/activity/activity_controller.scss +++ b/addons/mail/static/src/views/web/activity/activity_controller.scss @@ -90,20 +90,12 @@ font-size: large; } - > div { - max-width: 200px; - - .o_text_block { - @include o-text-overflow; - display: block; - } - } - .o_text_bold { - font-weight: bold; + font-weight: $font-weight-bold; } .o_text_block { + @include o-text-overflow($max-width: 15rem); display: block; } } diff --git a/addons/mail/static/src/views/web/activity/activity_renderer.xml b/addons/mail/static/src/views/web/activity/activity_renderer.xml index 6a6e64ed554..1c9ec4786cc 100644 --- a/addons/mail/static/src/views/web/activity/activity_renderer.xml +++ b/addons/mail/static/src/views/web/activity/activity_renderer.xml @@ -100,7 +100,7 @@ - + Schedule activity diff --git a/addons/mail/views/res_partner_views.xml b/addons/mail/views/res_partner_views.xml index 50ce11019bc..9a531c8cf64 100644 --- a/addons/mail/views/res_partner_views.xml +++ b/addons/mail/views/res_partner_views.xml @@ -81,8 +81,8 @@
- - + +
diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml index 8ebb94a64e7..8b4136c139f 100644 --- a/addons/maintenance/views/maintenance_views.xml +++ b/addons/maintenance/views/maintenance_views.xml @@ -52,6 +52,27 @@ + + maintenance.request.view.activity + maintenance.request + + + + +
+ +
+
+ +
+ +
+
+
+
+
+
+ equipment.request.form maintenance.request diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index de8769366c1..9367bef5bf9 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -19,6 +19,24 @@ fullscreen + + mrp.production.view.activity + mrp.production + + + +
+ +
+ + +
+
+
+
+
+
+ mrp.production.tree mrp.production diff --git a/addons/product/views/product_template_views.xml b/addons/product/views/product_template_views.xml index f154b45449e..50a8ab5e877 100644 --- a/addons/product/views/product_template_views.xml +++ b/addons/product/views/product_template_views.xml @@ -131,8 +131,8 @@
- -
+ +
[]
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index 43ecf8a1809..cb51ad41da3 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -483,8 +483,8 @@
- -
+ +
[]
diff --git a/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.js b/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.js index f04570d5c37..c7ed3ef40de 100644 --- a/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.js +++ b/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.js @@ -131,14 +131,14 @@ export class ProjectTaskStateSelection extends StateSelectionField { } getDropdownPosition() { - if (this.isView(['kanban', 'list', 'calendar']) || this.env.isSmall) { + if (this.isView(['activity', 'kanban', 'list', 'calendar']) || this.env.isSmall) { return ''; } return 'bottom-end'; } getTogglerClass(currentValue) { - if (this.isView(['kanban', 'list', 'calendar']) || this.env.isSmall) { + if (this.isView(['activity', 'kanban', 'list', 'calendar']) || this.env.isSmall) { return 'btn btn-link d-flex p-0'; } return 'o_state_button btn rounded-pill ' + this.colorButton[currentValue]; diff --git a/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.xml b/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.xml index 4dcd2bcdbff..b04e70db622 100644 --- a/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.xml +++ b/addons/project/static/src/components/project_task_state_selection/project_task_state_selection.xml @@ -13,7 +13,7 @@ - + @@ -22,13 +22,13 @@ - -
-
- AddTodo -
-
- Add a To-do - -
-
- - - - - -
-
-
- - - diff --git a/addons/project_todo/static/tests/activity_menu_tests.js b/addons/project_todo/static/tests/activity_menu_tests.js deleted file mode 100644 index 4ff71e9e9ae..00000000000 --- a/addons/project_todo/static/tests/activity_menu_tests.js +++ /dev/null @@ -1,77 +0,0 @@ -/* @odoo-module */ - -import { start } from "@mail/../tests/helpers/test_utils"; - -import { editInput, patchDate } from "@web/../tests/helpers/utils"; -import { click, contains, insertText } from "@web/../tests/utils"; - -const { DateTime } = luxon; - -QUnit.test("create todo from activity menu without date", async function () { - await start(); - await contains(".o_menu_systray i[aria-label='Activities']"); - await contains(".o-mail-ActivityMenu-counter", { count: 0 }); - await click(".o_menu_systray i[aria-label='Activities']"); - await contains(".o-mail-ActivityMenu", { - text: "Congratulations, you're done with your activities.", - }); - await click(".btn", { text: "Add a To-do" }); - await contains(".btn", { count: 0, text: "Add a To-do" }); - - await contains(".o-mail-ActivityMenu-show"); - await insertText("input.o-mail-ActivityMenu-input", "New To-do"); - await click(".btn", { text: "SAVE" }); - await contains(".o_notification", { - text: "Your to-do has been successfully added to your tasks and scheduled for completion.", - }); - // Need to reopen systray as it automatically closes when a todo is created - await click(".o_menu_systray i[aria-label='Activities']"); - await contains(".o-mail-ActivityMenu-counter", { text: "1" }); - await contains(".o-mail-ActivityGroup span", { text: "project.task" }); - await contains(".span", { text: "1 Today" }); - await contains(".btn", { text: "Add a To-do" }); - await contains(".o-mail-ActivityMenu-show", { count: 0 }); - await contains(".o-mail-ActivityMenu-input", { count: 0 }); - await click(".btn", { text: "Add a To-do" }); - await contains(".btn", { count: 0, text: "Add a To-do" }); - - await insertText("input.o-mail-ActivityMenu-input", "New To-do"); - await click(".btn", { text: "SAVE" }); - // Need to reopen systray as it automatically closes when a todo is created - await contains(".o_notification", { - count: 2, - text: "Your to-do has been successfully added to your tasks and scheduled for completion.", - }); - await click(".o_menu_systray i[aria-label='Activities']"); - await contains(".o-mail-ActivityMenu-counter", { text: "2" }); - await contains(".span", { text: "2 Today" }); -}); - -QUnit.test("create to-do from activity menu with date", async function () { - patchDate(2023, 1, 1, 6, 0, 0); - const futureDay = DateTime.utc().plus({ days: 2 }); - await start(); - await contains(".o_menu_systray i[aria-label='Activities']"); - await contains(".o-mail-ActivityMenu-counter", { count: 0 }); - - await click(".o_menu_systray i[aria-label='Activities']"); - await click(".btn", { text: "Add a To-do" }); - await insertText("input.o-mail-ActivityMenu-input", "New To-do"); - await editInput( - document.body, - "input.o_datetime_input", - futureDay.toString(luxon.DateTime.DATE_SHORT), - { - replace: true, - } - ); - await click(".btn", { text: "SAVE" }); - await contains(".o_notification", { - text: "Your to-do has been successfully added to your tasks and scheduled for completion.", - }); - // Need to reopen systray as it automatically closes when a todo is created - await click(".o_menu_systray i[aria-label='Activities']"); - await contains(".o-mail-ActivityMenu-counter", { text: "1" }); - await contains(".o-mail-ActivityGroup span", { text: "project.task" }); - await contains(".span", { text: "1 Future" }); -}); diff --git a/addons/project_todo/static/tests/helpers/mock_server/controller/project_todo.js b/addons/project_todo/static/tests/helpers/mock_server/controller/project_todo.js deleted file mode 100644 index e4d8fbefb55..00000000000 --- a/addons/project_todo/static/tests/helpers/mock_server/controller/project_todo.js +++ /dev/null @@ -1,44 +0,0 @@ -/** @odoo-module **/ - -import { patch } from "@web/core/utils/patch"; -import { serializeDate } from "@web/core/l10n/dates"; -import { MockServer } from '@web/../tests/helpers/mock_server'; - -const { DateTime } = luxon; - -patch(MockServer.prototype, { - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - async _performRPC(route, args) { - if (route === '/project_todo/new') { - return this._mockRouteTodoNew(args); - } - return super._performRPC(...arguments); - }, - - //-------------------------------------------------------------------------- - // Private Mocked Routes - //-------------------------------------------------------------------------- - - /** - * Simulates the `/project_todo/new` route. - * - * @private - */ - _mockRouteTodoNew(values) { - const taskId = this.pyEnv['project.task'].create({ description: values['todo_description'] }); - if (values['date_deadline']) { - this.pyEnv['mail.activity'].create({ - date_deadline: serializeDate(DateTime.fromISO(values['date_deadline'])), - note: values['todo_description'], - res_model: 'project.task', - res_id: taskId, - }); - } - }, -}); diff --git a/addons/project_todo/static/tests/helpers/model_definitions_setup.js b/addons/project_todo/static/tests/helpers/model_definitions_setup.js deleted file mode 100644 index d0fb9c5300a..00000000000 --- a/addons/project_todo/static/tests/helpers/model_definitions_setup.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @odoo-module **/ - -import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers'; - -addModelNamesToFetch(['project.task']); From 5f4add917a55f62c024a7a79c9240dc628a9f14f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Dufays Date: Wed, 11 Oct 2023 10:36:58 +0200 Subject: [PATCH 6/6] [IMP] crm, {test_}mail: allows to filter record having "done" activity To allow to search records having "done" activity (archived) assigned to someone, we have modified the search method of the field activity_user_id (in the activity mixin) so that it matches also responsible of archived activities. This field is mainly used when coming from the systray to display user activities including "done" ones but could be also used in custom filters. We have also modified the default domain when fetching record for the activity view so that it matches records having at least one activity, archived or not. This prevents to load more records than necessary to display the activity view. We also adapt the test accordingly. Task-3300854 Part-of: odoo/odoo#138135 --- addons/crm/views/crm_lead_views.xml | 3 +- addons/mail/models/mail_activity_mixin.py | 9 +- .../mail/static/src/core/web/activity_menu.js | 2 +- .../src/views/web/activity/activity_model.js | 4 +- .../mock_server/models/mail_activity.js | 218 +++++++++++++----- .../test_mail/static/tests/activity_tests.js | 31 ++- .../tests/systray_activity_menu_tests.js | 2 +- 7 files changed, 206 insertions(+), 63 deletions(-) diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 6dbd85964d1..a8f3bd2dc6b 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -1123,7 +1123,8 @@ crm.lead tree,kanban,graph,pivot,calendar,form,activity - [('activity_ids','!=',False)] + + [("activity_ids.active", "in", [True, False])] {'default_type': 'opportunity', 'search_default_assigned_to_me': 1} diff --git a/addons/mail/models/mail_activity_mixin.py b/addons/mail/models/mail_activity_mixin.py index 4350af7ec57..aeb1f543ba5 100644 --- a/addons/mail/models/mail_activity_mixin.py +++ b/addons/mail/models/mail_activity_mixin.py @@ -62,7 +62,7 @@ class MailActivityMixin(models.AbstractModel): 'Today: Activity date is today\nPlanned: Future activities.') activity_user_id = fields.Many2one( 'res.users', 'Responsible User', - related='activity_ids.user_id', readonly=False, + compute='_compute_activity_user_id', readonly=True, search='_search_activity_user_id', groups="base.group_user") activity_type_id = fields.Many2one( @@ -111,6 +111,11 @@ class MailActivityMixin(models.AbstractModel): record.activity_exception_decoration = exception_activity_type_id and exception_activity_type_id.decoration_type record.activity_exception_icon = exception_activity_type_id and exception_activity_type_id.icon + @api.depends('activity_ids.user_id') + def _compute_activity_user_id(self): + for record in self: + record.activity_user_id = record.activity_ids[0].user_id if record.activity_ids else False + def _search_activity_exception_decoration(self, operator, operand): return [('activity_ids.activity_type_id.decoration_type', operator, operand)] @@ -204,7 +209,7 @@ class MailActivityMixin(models.AbstractModel): @api.model def _search_activity_user_id(self, operator, operand): - return [('activity_ids.user_id', operator, operand)] + return [('activity_ids', 'any', [('active', 'in', [True, False]), ('user_id', operator, operand)])] @api.model def _search_activity_type_id(self, operator, operand): diff --git a/addons/mail/static/src/core/web/activity_menu.js b/addons/mail/static/src/core/web/activity_menu.js index 700e9112bfe..72aa7b2e191 100644 --- a/addons/mail/static/src/core/web/activity_menu.js +++ b/addons/mail/static/src/core/web/activity_menu.js @@ -55,7 +55,7 @@ export class ActivityMenu extends Component { // So, duplicates are faking the count and "Load more" doesn't show up force_search_count: 1, }; - let domain = [["activity_ids.user_id", "=", this.userId]]; + 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/activity/activity_model.js b/addons/mail/static/src/views/web/activity/activity_model.js index bdb1ea60445..489eabac05a 100644 --- a/addons/mail/static/src/views/web/activity/activity_model.js +++ b/addons/mail/static/src/views/web/activity/activity_model.js @@ -7,7 +7,9 @@ export class ActivityModel extends RelationalModel { async load(params = {}) { this.originalDomain = params.domain ? [...params.domain] : []; - params.domain?.push(["activity_ids", "!=", false]); + // Ensure that only (active) records with at least one activity, "done" (archived) or not, are fetched. + // We don't use active_test=false in the context because otherwise we would also get archived records. + params.domain?.push(["activity_ids.active", "in", [true, false]]); if (params && "groupBy" in params) { params.groupBy = []; } diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js b/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js index 915c733c927..d74a24ceace 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js @@ -1,9 +1,10 @@ /* @odoo-module */ -import { sortBy } from "@web/core/utils/arrays"; +import { Domain } from "@web/core/domain"; +import { groupBy, sortBy, unique } from "@web/core/utils/arrays"; import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -import { serializeDate, deserializeDate } from "@web/core/l10n/dates"; +import { deserializeDate } from "@web/core/l10n/dates"; const { DateTime} = luxon; @@ -26,7 +27,14 @@ patch(MockServer.prototype, { const domain = args.args[1] || args.kwargs.domain; const limit = args[2] || args.kwargs.limit || 0; const offset = args[3] || args.kwargs.offset || 0; - return this._mockMailActivityGetActivityData(res_model, domain, limit, offset); + const fetch_done = args[4] || args.kwargs.fetch_done || false; + return this._mockMailActivityGetActivityData( + res_model, + domain, + limit, + offset, + fetch_done + ); } return super._performRPC(route, args); }, @@ -87,7 +95,7 @@ patch(MockServer.prototype, { * @param {integer[]} ids * @returns {Object} */ - _mockMailActivityActionFeedback(ids) { + _mockMailActivityActionFeedback(ids, attachment_ids= null) { this._mockMailActivityActionDone(ids); }, /** @@ -118,63 +126,163 @@ patch(MockServer.prototype, { * @param {number} offset * @returns {Object} */ - _mockMailActivityGetActivityData(res_model, domain, limit = 0, offset = 0) { + /** + * Simulate partially (time zone not supported) `_compute_state_from_date` on `mail.activity`. + * + * @param {DateTime} date_deadline to convert into state + * @returns {string} dateline status (today, planned or overdue) + * @private + */ + _mockComputeStateFromDate(date_deadline) { + const now = DateTime.now(); + if (date_deadline.hasSame(now, "day")) { + return "today"; + } else if (date_deadline > now) { + return "planned"; + } + return "overdue"; + }, + /** + * Simulates `get_activity_data` on `mail.activity`. + * + * @private + * @param {string} res_model + * @param {string} domain + * @param {number} limit + * @param {number} offset + * @param {boolean} fetch_done + * @returns {Object} + */ + _mockMailActivityGetActivityData(res_model, domain, limit = 0, offset = 0, fetch_done = false) { const self = this; - const allRecords = this.getRecords(res_model, domain); + + // 1. Retrieve all ongoing and completed activities according to the parameters + const activityTypes = this.getRecords("mail.activity.type", [ + "|", + ["res_model", "=", res_model], + ["res_model", "=", false], + ]); + // Remove domain term used to filter record having "done" activities (not understood by the getRecords mock) + domain = Domain.removeDomainLeaves(new Domain(domain ?? []).toList(), [ + "activity_ids.active", + ]).toList(); + const allRecords = this.getRecords(res_model, domain ?? []); const records = limit ? allRecords.slice(offset, offset + limit) : allRecords; + const activityDomain = [["res_model", "=", res_model]]; + const isFiltered = domain || limit || offset; + const domainResIds = records.map((r) => r.id); + if (isFiltered) { + activityDomain.push(["res_id", "in", domainResIds]); + } + const allActivities = this.getRecords("mail.activity", activityDomain, { + active_test: !fetch_done, + }); + const allOngoing = allActivities.filter((a) => a.active); + const allCompleted = allActivities.filter((a) => !a.active); - const activityTypes = this.getRecords("mail.activity.type", []); - const activityIds = records.map((x) => x.activity_ids).flat(); + // 2. Get attachment of completed activities + let attachmentsById; + if (allCompleted.length) { + const attachmentIds = allCompleted.map((a) => a.attachment_ids).flat(); + attachmentsById = attachmentIds.length + ? Object.fromEntries( + this.getRecords("ir.attachment", [["id", "in", attachmentIds]]).map((a) => [a.id, a]) + ) + : {}; + } else { + attachmentsById = {}; + } - const groupedActivities = {}; + // 3. Group activities per records and activity type + const groupedCompleted = groupBy(allCompleted, (a) => [a.res_id, a.activity_type_id]); + const groupedOngoing = groupBy(allOngoing, (a) => [a.res_id, a.activity_type_id]); + + // 4. Format data const resIdToDeadline = {}; - const groups = self.mockReadGroup("mail.activity", { - domain: [["id", "in", activityIds]], - fields: [ - "res_id", - "activity_type_id", - "ids:array_agg(id)", - "date_deadline:min(date_deadline)", - ], - groupby: ["res_id", "activity_type_id"], - lazy: false, - }); - groups.forEach(function (group) { - // mockReadGroup doesn't correctly return all asked fields - const activites = self.getRecords("mail.activity", group.__domain); - group.activity_type_id = group.activity_type_id[0]; - let minDate; - activites.forEach(function (activity) { - if (!minDate || deserializeDate(activity.date_deadline) < deserializeDate(minDate)) { - minDate = activity.date_deadline; - } - }); - group.date_deadline = minDate; - resIdToDeadline[group.res_id] = minDate; - let state; - if (group.date_deadline === serializeDate(DateTime.now())) { - state = "today"; - } else if (deserializeDate(group.date_deadline) > DateTime.now()) { - state = "planned"; - } else { - state = "overdue"; + const resIdToDateDone = {}; + const groupedActivities = {}; + for (const resIdStrTuple of new Set([ + ...Object.keys(groupedCompleted), + ...Object.keys(groupedOngoing), + ])) { + const [resId, activityTypeId] = resIdStrTuple.split(",").map((n) => Number(n)); + const ongoing = groupedOngoing[resIdStrTuple] || []; + const completed = groupedCompleted[resIdStrTuple] || []; + const dateDone = completed.length + ? DateTime.max(...completed.map((a) => deserializeDate(a.date_done))) + : false; + const dateDeadline = ongoing.length + ? DateTime.min(...ongoing.map((a) => deserializeDate(a.date_deadline))) + : false; + if ( + dateDeadline && + (resIdToDeadline[resId] === undefined || dateDeadline < resIdToDeadline[resId]) + ) { + resIdToDeadline[resId] = dateDeadline; } - if (!groupedActivities[group.res_id]) { - groupedActivities[group.res_id] = {}; + if ( + dateDone && + (resIdToDateDone[resId] === undefined || dateDone > resIdToDateDone[resId]) + ) { + resIdToDateDone[resId] = dateDone; } - groupedActivities[group.res_id][group.activity_type_id] = { - count: group.__count, - state: state, - o_closest_deadline: group.date_deadline, - ids: activites.map((x) => x.id), + const userAssignedIds = unique( + sortBy( + ongoing.filter((a) => a.user_id), + (a) => a.date_deadline + ).map((a) => a.user_id) + ); + const reportingDate = ongoing.length ? dateDeadline : dateDone; + const attachments = completed + .map((act) => act.attachment_ids) + .flat() + .map((attachmentId) => attachmentsById[attachmentId]); + const attachmentsInfo = {}; + if (attachments.length) { + const lastAttachmentCreateDate = DateTime.max( + ...attachments.map((a) => deserializeDate(a.create_date)) + ); + const mostRecentAttachment = attachments.find((a) => + lastAttachmentCreateDate.equals(deserializeDate(a.create_date)) + ); + attachmentsInfo.attachments = { + most_recent_id: mostRecentAttachment.id, + most_recent_name: mostRecentAttachment.name, + count: attachments.length, + }; + } + if (!(resId in groupedActivities)) { + groupedActivities[resId] = {}; + } + groupedActivities[resId][activityTypeId] = { + count_by_state: { + ...Object.fromEntries( + Object.entries( + groupBy(ongoing, (a) => + self._mockComputeStateFromDate(deserializeDate(a.date_deadline)) + ) + ).map(([state, activities]) => [state, activities.length]) + ), + ...(completed.length ? { done: completed.length } : {}), + }, + ids: ongoing.map((a) => a.id).concat(completed.map((a) => a.id)), + reporting_date: reportingDate ? reportingDate.toFormat("yyyy-LL-dd") : false, + state: ongoing.length ? self._mockComputeStateFromDate(dateDeadline) : "done", + user_assigned_ids: userAssignedIds, + ...attachmentsInfo, }; - }); + } + const ongoingResIds = sortBy(Object.keys(resIdToDeadline), (item) => resIdToDeadline[item]); + const completedResIds = sortBy( + Object.keys(resIdToDateDone).filter((resId) => !(resId in resIdToDeadline)), + (item) => resIdToDateDone[item] + ); return { activity_types: activityTypes.map((type) => { - let mailTemplates = []; + let templates = []; if (type.mail_template_ids) { - mailTemplates = type.mail_template_ids.map((template_id) => { + templates = type.mail_template_ids.map((template_id) => { const template = this.getRecords("mail.template", [ ["id", "=", template_id], ])[0]; @@ -184,12 +292,14 @@ patch(MockServer.prototype, { }; }); } - return [type.id, type.display_name, mailTemplates]; + return { + id: type.id, + name: type.display_name, + template_ids: templates, + keep_done: type.keep_done, + }; }), - activity_res_ids: sortBy( - records.map((x) => x.id), - (id) => deserializeDate(resIdToDeadline[id]) - ), + activity_res_ids: ongoingResIds.concat(completedResIds).map((idStr) => Number(idStr)), grouped_activities: groupedActivities, }; }, diff --git a/addons/test_mail/static/tests/activity_tests.js b/addons/test_mail/static/tests/activity_tests.js index a07528a6bc5..cde4946c552 100644 --- a/addons/test_mail/static/tests/activity_tests.js +++ b/addons/test_mail/static/tests/activity_tests.js @@ -6,7 +6,10 @@ import { ActivityModel } from "@mail/views/web/activity/activity_model"; import { ActivityRenderer } from "@mail/views/web/activity/activity_renderer"; import { start } from "@mail/../tests/helpers/test_utils"; +import { RelationalModel } from "@web/model/relational_model/relational_model"; +import { Domain } from "@web/core/domain"; import { serializeDate } from "@web/core/l10n/dates"; +import { deepEqual } from "@web/core/utils/objects"; import { session } from "@web/session"; import testUtils from "@web/../tests/legacy/helpers/test_utils"; import { editInput, patchWithCleanup, click, patchDate } from "@web/../tests/helpers/utils"; @@ -23,6 +26,28 @@ QUnit.module("test_mail", {}, function () { QUnit.module("activity view", { async beforeEach() { patchDate(2023, 4, 8, 10, 0, 0); + patchWithCleanup(RelationalModel.prototype, { + async load(params) { + if (params.domain) { + // Remove domain term used to filter record having "done" activities (not understood by the getRecords mock) + const domain = new Domain(params.domain); + const newDomain = Domain.removeDomainLeaves(domain.toList(), [ + "activity_ids.active", + ]); + if (!deepEqual(domain.toList(), newDomain.toList())) { + return super.load({ + ...params, + domain: newDomain.toList(), + context: params.context + ? { ...params.context, active_test: false } + : { active_test: false }, + }); + } + return super.load(params); + } + return super.load(params); + }, + }); pyEnv = await startServer(); const mailTemplateIds = pyEnv["mail.template"].create([ { name: "Template1" }, @@ -186,7 +211,7 @@ QUnit.module("test_mail", {}, function () { 'should contain "Meeting Room Furnitures" in first colum of second row' ); - const today = DateTime.now().toLocaleString({ day: "numeric", month: "short" }); + const today = DateTime.now().toLocaleString(luxon.DateTime.DATE_SHORT); assert.ok( $activity.find( @@ -380,8 +405,8 @@ QUnit.module("test_mail", {}, function () { }); assert.verifySteps([ - JSON.stringify([["activity_ids", "!=", false]]), - JSON.stringify([["activity_ids", "!=", false]]), + JSON.stringify([["activity_ids.active", "in", [true, false]]]), + '[[1,"=",1]]', // Due to the patch above that removes it ]); }); diff --git a/addons/test_mail/static/tests/systray_activity_menu_tests.js b/addons/test_mail/static/tests/systray_activity_menu_tests.js index 1819bedcac0..16a2638ae34 100644 --- a/addons/test_mail/static/tests/systray_activity_menu_tests.js +++ b/addons/test_mail/static/tests/systray_activity_menu_tests.js @@ -81,7 +81,7 @@ QUnit.test("activity menu widget: activity menu with 2 models", async (assert) = await contains(".o-mail-ActivityMenu-counter", { text: "5" }); const actionChecks = { context: { force_search_count: 1 }, - domain: [["activity_ids.user_id", "=", session.uid]], + domain: [["activity_user_id", "=", session.uid]], }; patchWithCleanup(env.services.action, { doAction(action) {