+ % if not recurrent:
|
${object.event_id.get_interval('dayname', tz=object.partner_id.tz if not object.event_id.allday else None)}
@@ -43,6 +46,7 @@
|
|
+ % endif
Details of the event
@@ -54,6 +58,9 @@
% if object.event_id.description :
- Description: ${object.event_id.description}
% endif
+ % if recurrent:
+ - When: ${object.recurrence_id.name}
+ % endif
% if not object.event_id.allday and object.event_id.duration
- Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}
% endif
@@ -95,16 +102,18 @@
${object.partner_id.lang}
- % set colors = {'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}
+ % set colors = ctx.get('colors', {})
+ % set recurrent = object.recurrence_id and not ctx['ignore_recurrence']
Hello ${object.common_name},
The date of the meeting has been updated. The meeting ${object.event_id.name} created by ${object.event_id.user_id.partner_id.name} is now scheduled for ${object.event_id.get_display_time_tz(tz=object.partner_id.tz)}.
+ % if not recurrent:
|
${object.event_id.get_interval('dayname', tz=object.partner_id.tz if not object.event_id.allday else None)}
@@ -127,6 +137,7 @@
|
|
+ % endif
Details of the event
@@ -138,6 +149,9 @@
% if object.event_id.description :
- Description: ${object.event_id.description}
% endif
+ % if recurrent:
+ - When: ${object.recurrence_id.name}
+ % endif
% if not object.event_id.allday and object.event_id.duration
- Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}
% endif
diff --git a/addons/calendar/models/calendar_alarm_manager.py b/addons/calendar/models/calendar_alarm_manager.py
index 943073c11c2..98fe461b7cd 100644
--- a/addons/calendar/models/calendar_alarm_manager.py
+++ b/addons/calendar/models/calendar_alarm_manager.py
@@ -15,7 +15,7 @@ class AlarmManager(models.AbstractModel):
_name = 'calendar.alarm_manager'
_description = 'Event Alarm Manager'
- def _get_next_potential_limit_alarm(self, alarm_type, seconds=None, partner_id=None):
+ def _get_next_potential_limit_alarm(self, alarm_type, seconds=None, partners=None):
result = {}
delta_request = """
SELECT
@@ -50,16 +50,16 @@ class AlarmManager(models.AbstractModel):
filter_user = """
RIGHT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = cal.id
- AND part_rel.res_partner_id = %s
+ AND part_rel.res_partner_id IN %s
"""
# Add filter on alarm type
tuple_params = (alarm_type,)
# Add filter on partner_id
- if partner_id:
+ if partners:
base_request += filter_user
- tuple_params += (partner_id, )
+ tuple_params += (tuple(partners.ids), )
# Upper bound on first_alarm of requested events
first_alarm_max_value = ""
@@ -117,10 +117,10 @@ class AlarmManager(models.AbstractModel):
"""
result = []
# TODO: remove event_maxdelta and if using it
- if one_date - timedelta(minutes=(missing and 0 or event_maxdelta)) < datetime.datetime.now() + timedelta(seconds=in_the_next_X_seconds): # if an alarm is possible for this date
+ if one_date - timedelta(minutes=(missing and 0 or event_maxdelta)) < fields.Datetime.now() + timedelta(seconds=in_the_next_X_seconds): # if an alarm is possible for this date
for alarm in event.alarm_ids:
if alarm.alarm_type == alarm_type and \
- one_date - timedelta(minutes=(missing and 0 or alarm.duration_minutes)) < datetime.datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \
+ one_date - timedelta(minutes=(missing and 0 or alarm.duration_minutes)) < fields.Datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \
(not after or one_date - timedelta(minutes=alarm.duration_minutes) > fields.Datetime.from_string(after)):
alert = {
'alarm_id': alarm.id,
@@ -132,6 +132,10 @@ class AlarmManager(models.AbstractModel):
@api.model
def get_next_mail(self):
+ return self._get_partner_next_mail(partners=None)
+
+ @api.model
+ def _get_partner_next_mail(self, partners=None):
last_notif_mail = fields.Datetime.to_string(self.env.context.get('lastcall') or fields.Datetime.now())
cron = self.env.ref('calendar.ir_cron_scheduler_alarm', raise_if_not_found=False)
@@ -153,7 +157,7 @@ class AlarmManager(models.AbstractModel):
cron_interval = cron.interval_number * interval_to_second[cron.interval_type]
- all_meetings = self._get_next_potential_limit_alarm('email', seconds=cron_interval)
+ all_meetings = self._get_next_potential_limit_alarm('email', seconds=cron_interval, partners=partners)
for meeting in self.env['calendar.event'].browse(all_meetings):
max_delta = all_meetings[meeting.id]['max_duration']
@@ -183,7 +187,7 @@ class AlarmManager(models.AbstractModel):
if not partner:
return []
- all_meetings = self._get_next_potential_limit_alarm('notification', partner_id=partner.id)
+ all_meetings = self._get_next_potential_limit_alarm('notification', partners=partner)
time_limit = 3600 * 24 # return alarms of the next 24 hours
for event_id in all_meetings:
max_delta = all_meetings[event_id]['max_duration']
@@ -215,7 +219,7 @@ class AlarmManager(models.AbstractModel):
result = False
if alarm.alarm_type == 'email':
- result = meeting.attendee_ids.filtered(lambda r: r.state != 'declined')._send_mail_to_attendees('calendar.calendar_template_meeting_reminder', force_send=True, force_event_id=meeting)
+ result = meeting.attendee_ids.filtered(lambda r: r.state != 'declined')._send_mail_to_attendees('calendar.calendar_template_meeting_reminder', force_send=True, ignore_recurrence=True)
return result
def do_notif_reminder(self, alert):
@@ -225,7 +229,7 @@ class AlarmManager(models.AbstractModel):
if alarm.alarm_type == 'notification':
message = meeting.display_time
- delta = alert['notify_at'] - datetime.datetime.now()
+ delta = alert['notify_at'] - fields.Datetime.now()
delta = delta.seconds + delta.days * 3600 * 24
return {
diff --git a/addons/calendar/models/calendar_attendee.py b/addons/calendar/models/calendar_attendee.py
index 765258274f0..375a18a1057 100644
--- a/addons/calendar/models/calendar_attendee.py
+++ b/addons/calendar/models/calendar_attendee.py
@@ -2,10 +2,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import uuid
import base64
+import logging
+from collections import defaultdict
from odoo import api, fields, models, _
from odoo.exceptions import UserError
+_logger = logging.getLogger(__name__)
+
class Attendee(models.Model):
""" Calendar Attendee Information """
@@ -24,43 +28,59 @@ class Attendee(models.Model):
('accepted', 'Accepted'),
]
+ event_id = fields.Many2one('calendar.event', 'Meeting linked', required=True, ondelete='cascade')
+ partner_id = fields.Many2one('res.partner', 'Contact', required=True, readonly=True)
state = fields.Selection(STATE_SELECTION, string='Status', readonly=True, default='needsAction',
help="Status of the attendee's participation")
common_name = fields.Char('Common name', compute='_compute_common_name', store=True)
- partner_id = fields.Many2one('res.partner', 'Contact', readonly=True)
- email = fields.Char('Email', help="Email of Invited Person")
+ email = fields.Char('Email', related='partner_id.email', help="Email of Invited Person")
availability = fields.Selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly=True)
access_token = fields.Char('Invitation Token', default=_default_access_token)
- event_id = fields.Many2one('calendar.event', 'Meeting linked', ondelete='cascade')
+ recurrence_id = fields.Many2one('calendar.recurrence', related='event_id.recurrence_id')
@api.depends('partner_id', 'partner_id.name', 'email')
def _compute_common_name(self):
for attendee in self:
attendee.common_name = attendee.partner_id.name or attendee.email
- @api.onchange('partner_id')
- def _onchange_partner_id(self):
- """ Make entry on email and availability on change of partner_id field. """
- self.email = self.partner_id.email
-
@api.model_create_multi
def create(self, vals_list):
for values in vals_list:
+ if values.get('partner_id') == self.env.user.partner_id.id:
+ values['state'] = 'accepted'
if not values.get("email") and values.get("common_name"):
common_nameval = values.get("common_name").split(':')
email = [x for x in common_nameval if '@' in x] # TODO JEM : should be refactored
values['email'] = email and email[0] or ''
values['common_name'] = values.get("common_name")
- return super(Attendee, self).create(vals_list)
+ attendees = super().create(vals_list)
+ attendees._subscribe_partner()
+ return attendees
+
+ def unlink(self):
+ self._unsubscribe_partner()
+ return super().unlink()
+
+ def _subscribe_partner(self):
+ for event in self.event_id:
+ partners = (event.attendee_ids & self).partner_id - event.message_partner_ids
+ partners -= self.env.user.partner_id # current user is automatically added as followers, don't add it twice.
+ event.message_subscribe(partner_ids=partners.ids)
+
+ def _unsubscribe_partner(self):
+ for event in self.event_id:
+ partners = (event.attendee_ids & self).partner_id & event.message_partner_ids
+ event.message_unsubscribe(partner_ids=partners.ids)
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
raise UserError(_('You cannot duplicate a calendar attendee.'))
- def _send_mail_to_attendees(self, template_xmlid, force_send=False, force_event_id=None):
+ def _send_mail_to_attendees(self, template_xmlid, force_send=False, ignore_recurrence=False):
""" Send mail for event invitation to event attendees.
:param template_xmlid: xml id of the email template to use to send the invitation
:param force_send: if set to True, the mail(s) will be sent immediately (instead of the next queue processing)
+ :param ignore_recurrence: ignore event recurrence
"""
res = False
@@ -68,10 +88,12 @@ class Attendee(models.Model):
return res
calendar_view = self.env.ref('calendar.view_calendar_event_calendar')
- invitation_template = self.env.ref(template_xmlid)
-
+ invitation_template = self.env.ref(template_xmlid, raise_if_not_found=False)
+ if not invitation_template:
+ _logger.warning("Template %s could not be found. %s not notified." % (template_xmlid, self))
+ return
# get ics file for all meetings
- ics_files = force_event_id._get_ics_file() if force_event_id else self.mapped('event_id')._get_ics_file()
+ ics_files = self.mapped('event_id')._get_ics_file()
# prepare rendering context for mail template
colors = {
@@ -82,40 +104,41 @@ class Attendee(models.Model):
}
rendering_context = dict(self._context)
rendering_context.update({
- 'color': colors,
+ 'colors': colors,
+ 'ignore_recurrence': ignore_recurrence,
'action_id': self.env['ir.actions.act_window'].search([('view_id', '=', calendar_view.id)], limit=1).id,
'dbname': self._cr.dbname,
'base_url': self.env['ir.config_parameter'].sudo().get_param('web.base.url', default='http://localhost:8069'),
- 'force_event_id': force_event_id,
})
- invitation_template = invitation_template.with_context(rendering_context)
- # send email with attachments
- mail_ids = []
for attendee in self:
- if attendee.email or attendee.partner_id.email:
+ if attendee.email and attendee.partner_id != self.env.user.partner_id:
# FIXME: is ics_file text or bytes?
- event_id = force_event_id.id if force_event_id else attendee.event_id.id
+ event_id = attendee.event_id.id
ics_file = ics_files.get(event_id)
- email_values = {
- 'model': None, # We don't want to have the mail in the tchatter while in queue!
- 'res_id': None,
- }
if ics_file:
- email_values['attachment_ids'] = [
+ attachment_values = [
(0, 0, {'name': 'invitation.ics',
'mimetype': 'text/calendar',
'datas': base64.b64encode(ics_file)})
]
- mail_ids.append(invitation_template.with_context(no_document=True).send_mail(attendee.id, email_values=email_values, notif_layout='mail.mail_notification_light'))
- else:
- mail_ids.append(invitation_template.send_mail(attendee.id, email_values=email_values, notif_layout='mail.mail_notification_light'))
-
- if force_send and mail_ids:
- res = self.env['mail.mail'].browse(mail_ids).send()
-
- return res
+ body = self.env['mail.template'].with_context(rendering_context)._render_template(
+ invitation_template.body_html,
+ self._name,
+ attendee.ids)
+ # TODO check lang
+ subject = self.env['mail.template']._render_template(
+ invitation_template.subject,
+ self._name,
+ attendee.ids)
+ attendee.event_id.with_context(no_document=True).message_notify(
+ body=body,
+ subject=subject,
+ partner_ids=attendee.partner_id.ids,
+ email_layout_xmlid='mail.mail_notification_light',
+ attachment_ids=attachment_values,
+ force_send=force_send)
def do_tentative(self):
""" Makes event invitation as Tentative. """
@@ -123,17 +146,13 @@ class Attendee(models.Model):
def do_accept(self):
""" Marks event invitation as Accepted. """
- result = self.write({'state': 'accepted'})
for attendee in self:
- if attendee.event_id:
- attendee.event_id.message_post(body=_("%s has accepted invitation") % (attendee.common_name), subtype_xmlid="calendar.subtype_invitation")
- return result
+ attendee.event_id.message_post(body=_("%s has accepted invitation") % (attendee.common_name), subtype_xmlid="calendar.subtype_invitation")
+ return self.write({'state': 'accepted'})
def do_decline(self):
""" Marks event invitation as Declined. """
- res = self.write({'state': 'declined'})
for attendee in self:
- if attendee.event_id:
- attendee.event_id.message_post(body=_("%s has declined invitation") % (attendee.common_name), subtype_xmlid="calendar.subtype_invitation")
- return res
+ attendee.event_id.message_post(body=_("%s has declined invitation") % (attendee.common_name), subtype_xmlid="calendar.subtype_invitation")
+ return self.write({'state': 'declined'})
diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py
index 84842effcae..98d92296257 100644
--- a/addons/calendar/models/calendar_event.py
+++ b/addons/calendar/models/calendar_event.py
@@ -448,61 +448,37 @@ class Meeting(models.Model):
return result
- def create_attendees(self):
- current_user = self.env.user
- result = {}
- for meeting in self:
- alreay_meeting_partners = meeting.attendee_ids.mapped('partner_id')
- meeting_attendees = self.env['calendar.attendee']
- meeting_partners = self.env['res.partner']
- for partner in meeting.partner_ids.filtered(lambda partner: partner not in alreay_meeting_partners):
- values = {
- 'partner_id': partner.id,
- 'email': partner.email,
- 'event_id': meeting.id,
- }
+ def _attendees_values(self, partner_commands):
+ """
+ :param partner_commands: ORM commands for partner_id field (0 and 1 commands not supported)
+ :return: associated attendee_ids ORM commands
+ """
+ attendee_commands = []
- # TODO move this in the correct module
- if self._context.get('google_internal_event_id', False):
- values['google_internal_event_id'] = self._context.get('google_internal_event_id')
+ removed_partner_ids = []
+ added_partner_ids = []
+ for command in partner_commands:
+ op = command[0]
+ if op in (2, 3): # Remove partner
+ removed_partner_ids += [command[1]]
+ elif op == 6: # Replace all
+ removed_partner_ids += set(self.partner_ids.ids) - set(command[2]) # Don't recreate attendee if partner already attend the event
+ added_partner_ids += set(command[2]) - set(self.partner_ids.ids)
+ elif op == 4:
+ added_partner_ids += [command[1]] if command[1] not in self.partner_ids.ids else []
+ # commands 0 and 1 not supported
- # current user don't have to accept his own meeting
- if partner == self.env.user.partner_id:
- values['state'] = 'accepted'
+ attendees_to_unlink = self.env['calendar.attendee'].search([
+ ('event_id', 'in', self.ids),
+ ('partner_id', 'in', removed_partner_ids),
+ ])
+ attendee_commands += [[2, attendee.id] for attendee in attendees_to_unlink] # Removes and delete
- attendee = self.env['calendar.attendee'].create(values)
-
- meeting_attendees |= attendee
- meeting_partners |= partner
-
- if meeting_attendees and not self._context.get('detaching'):
- to_notify = meeting_attendees.filtered(lambda a: a.email != current_user.email)
- to_notify._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
-
- if meeting_attendees:
- meeting.write({'attendee_ids': [(4, meeting_attendee.id) for meeting_attendee in meeting_attendees]})
-
- if meeting_partners:
- meeting.message_subscribe(partner_ids=meeting_partners.ids)
-
- # We remove old attendees who are not in partner_ids now.
- all_partners = meeting.partner_ids
- all_partner_attendees = meeting.attendee_ids.mapped('partner_id')
- old_attendees = meeting.attendee_ids
- partners_to_remove = all_partner_attendees + meeting_partners - all_partners
-
- attendees_to_remove = self.env["calendar.attendee"]
- if partners_to_remove:
- attendees_to_remove = self.env["calendar.attendee"].search([('partner_id', 'in', partners_to_remove.ids), ('event_id', '=', meeting.id)])
- attendees_to_remove.unlink()
-
- result[meeting.id] = {
- 'new_attendees': meeting_attendees,
- 'old_attendees': old_attendees,
- 'removed_attendees': attendees_to_remove,
- 'removed_partners': partners_to_remove
- }
- return result
+ attendee_commands += [
+ [0, 0, dict(partner_id=partner_id)]
+ for partner_id in added_partner_ids
+ ]
+ return attendee_commands
def get_interval(self, interval, tz=None):
""" Format and localize some dates to be used in email templates
@@ -646,6 +622,11 @@ class Meeting(models.Model):
break_recurrence = values.get('recurrency') is False
self._sync_activities(values)
+ if 'partner_ids' in values:
+ values['attendee_ids'] = self._attendees_values(values['partner_ids'])
+
+ previous_attendees = self.attendee_ids
+
recurrence_values = {field: values.pop(field) for field in self._get_recurrent_fields() if field in values}
if update_recurrence:
if break_recurrence:
@@ -660,39 +641,24 @@ class Meeting(models.Model):
else:
super().write(values)
+ if recurrence_update_setting != 'self_only' and not break_recurrence:
+ detached_events |= self._apply_recurrence_values(recurrence_values, future=recurrence_update_setting == 'future_events')
- for meeting in self:
-
- attendees_create = False
- if values.get('partner_ids'):
- attendees_create = self.with_context(dont_notify=True).create_attendees() # to prevent multiple _notify_next_alarm
-
- # Notify attendees if there is an alarm on the modified event, or if there was an alarm
- # that has just been removed, as it might have changed their next event notification
- if not self._context.get('dont_notify'):
- if len(meeting.alarm_ids) > 0 or values.get('alarm_ids'):
- partners_to_notify = meeting.partner_ids.ids
- event_attendees_changes = attendees_create and meeting and attendees_create[meeting.id]
- if event_attendees_changes:
- partners_to_notify.extend(event_attendees_changes['removed_partners'].ids)
- self.env['calendar.alarm_manager']._notify_next_alarm(partners_to_notify)
-
- if (values.get('start_date') or values.get('start_datetime') or
- (values.get('start') and self.env.context.get('from_ui'))) and values.get('active', True):
- for current_meeting in self:
- if attendees_create:
- attendees_create = attendees_create[current_meeting.id]
- attendee_to_email = attendees_create['old_attendees'] - attendees_create['removed_attendees']
- else:
- attendee_to_email = current_meeting.attendee_ids
-
- if attendee_to_email:
- attendee_to_email._send_mail_to_attendees('calendar.calendar_template_meeting_changedate')
-
- if recurrence_update != 'self_only' and not break_recurrence:
- detached_events |= self._apply_recurrence_values(recurrence_values, future=recurrence_update == 'future_events')
(detached_events & self).active = False
(detached_events - self).unlink()
+
+ # Notify attendees if there is an alarm on the modified event, or if there was an alarm
+ # that has just been removed, as it might have changed their next event notification
+ if not self._context.get('dont_notify'):
+ if self.alarm_ids or values.get('alarm_ids'):
+ self.env['calendar.alarm_manager']._notify_next_alarm(self.partner_ids.ids)
+
+ current_attendees = self.filtered('active').attendee_ids
+ if 'partner_ids' in values:
+ (current_attendees - previous_attendees)._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
+ if 'start' in values:
+ (current_attendees & previous_attendees)._send_mail_to_attendees('calendar.calendar_template_meeting_changedate', ignore_recurrence=not update_recurrence)
+
return True
@api.model # LUL TODO create multi
@@ -719,6 +685,9 @@ class Meeting(models.Model):
activity_vals['user_id'] = user_id
values['activity_ids'] = [(0, 0, activity_vals)]
+ if 'partner_ids' in values:
+ values['attendee_ids'] = self._attendees_values(values['partner_ids'])
+
recurrence_values = {field: values.pop(field) for field in self._get_recurrent_fields() if field in values}
meeting = super(Meeting, self).create(values)
@@ -726,11 +695,9 @@ class Meeting(models.Model):
detached_events = meeting._apply_recurrence_values(recurrence_values)
detached_events.active = False
+ meeting.attendee_ids._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
meeting._sync_activities(values)
- # `dont_notify=True` in context to prevent multiple _notify_next_alarm
- meeting.with_context(dont_notify=True).create_attendees()
-
# Notify attendees if there is an alarm on the created event, as it might have changed their
# next event notification
if not self._context.get('dont_notify'):
diff --git a/addons/calendar/models/calendar_recurrence.py b/addons/calendar/models/calendar_recurrence.py
index 2553b43218d..acc5241ac42 100644
--- a/addons/calendar/models/calendar_recurrence.py
+++ b/addons/calendar/models/calendar_recurrence.py
@@ -90,8 +90,7 @@ class RecurrenceRule(models.Model):
_name = 'calendar.recurrence'
_description = 'Event Recurrence Rule'
- # TODO exdate?
-
+ name = fields.Char(compute='_compute_name', store=True)
base_event_id = fields.Many2one('calendar.event', ondelete='set null', copy=False)
calendar_event_ids = fields.One2many('calendar.event', 'recurrence_id')
event_tz = fields.Selection(_tz_get, string='Timezone', default=lambda self: self.env.context.get('tz') or self.env.user.tz)
@@ -118,6 +117,33 @@ class RecurrenceRule(models.Model):
('month_day', "CHECK (rrule_type != 'monthly' OR month_by != 'day' OR day >= 1 AND day <= 31)", "The day must be between 1 and 31"),
]
+ @api.depends('rrule')
+ def _compute_name(self):
+ for recurrence in self:
+ period = dict(RRULE_TYPE_SELECTION)[recurrence.rrule_type]
+ every = _("Every %s %s, ") % (recurrence.interval, period)
+
+ if recurrence.end_type == 'count':
+ end = _("for %s events") % recurrence.count
+ elif recurrence.end_type == 'end_date':
+ end = _("until %s") % recurrence.until
+ else:
+ end = ''
+
+ if recurrence.rrule_type == 'weeky':
+ weekdays = recurrence._get_week_days()
+ fields = (self._fields[weekday_to_field(w)] for w in weekdays)
+ on = _("on %s,") % ", ".join([field.string for field in fields])
+ elif recurrence.rrule_type == 'monthly':
+ if recurrence.month_by == 'day':
+ weekday_label = dict(BYDAY_SELECTION)[recurrence.byday]
+ on = _("on the %(position)s %(weekday)s, ") % {'position': recurrence.byday, 'weekday': weekday_label}
+ else:
+ on = _("day %s, ") % recurrence.day
+ else:
+ on = ''
+ recurrence.name = every + on + end
+
@api.depends('calendar_event_ids.start')
def _compute_dtstart(self):
groups = self.env['calendar.event'].read_group([('recurrence_id', 'in', self.ids)], ['start:min'], ['recurrence_id'])
diff --git a/addons/calendar/models/ir_http.py b/addons/calendar/models/ir_http.py
index f28088382a1..07441f15135 100644
--- a/addons/calendar/models/ir_http.py
+++ b/addons/calendar/models/ir_http.py
@@ -32,4 +32,4 @@ class IrHttp(models.AbstractModel):
if error_message:
raise BadRequest(error_message)
- return True
+ cls._auth_method_public()
diff --git a/addons/calendar/tests/__init__.py b/addons/calendar/tests/__init__.py
index 137c25cc007..71cfed3531b 100644
--- a/addons/calendar/tests/__init__.py
+++ b/addons/calendar/tests/__init__.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import test_attendees
from . import test_calendar
from . import test_calendar_recurrent_event_case2
from . import test_event_recurrence
+from . import test_event_notifications
diff --git a/addons/calendar/tests/test_attendees.py b/addons/calendar/tests/test_attendees.py
new file mode 100644
index 00000000000..7b3389b717c
--- /dev/null
+++ b/addons/calendar/tests/test_attendees.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime
+
+from odoo.tests.common import SavepointCase, new_test_user
+
+
+class TestEventNotifications(SavepointCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.event = cls.env['calendar.event'].create({
+ 'name': "Doom's day",
+ 'start': datetime(2019, 10, 25, 8, 0),
+ 'stop': datetime(2019, 10, 27, 18, 0),
+ }).with_context(mail_notrack=True)
+ cls.user = new_test_user(cls.env, 'xav', email='em@il.com', notification_type='inbox')
+ cls.partner = cls.user.partner_id
+
+ def test_attendee_added(self):
+ self.event.partner_ids = self.partner
+ self.assertTrue(self.event.attendee_ids, "It should have created an attendee")
+ self.assertEqual(self.event.attendee_ids.partner_id, self.partner, "It should be linked to the partner")
+ self.assertIn(self.partner, self.event.message_follower_ids.partner_id, "He should be follower of the event")
+
+ def test_attendee_added_create(self):
+ event = self.env['calendar.event'].create({
+ 'name': "Doom's day",
+ 'start': datetime(2019, 10, 25, 8, 0),
+ 'stop': datetime(2019, 10, 27, 18, 0),
+ 'partner_ids': [(4, self.partner.id)],
+ })
+ self.assertTrue(event.attendee_ids, "It should have created an attendee")
+ self.assertEqual(event.attendee_ids.partner_id, self.partner, "It should be linked to the partner")
+ self.assertIn(self.partner, event.message_follower_ids.partner_id, "He should be follower of the event")
+
+ def test_attendee_added_multi(self):
+ event = self.env['calendar.event'].create({
+ 'name': "Doom's day",
+ 'start': datetime(2019, 10, 25, 8, 0),
+ 'stop': datetime(2019, 10, 27, 18, 0),
+ })
+ events = self.event | event
+ events.partner_ids = self.partner
+ self.assertEqual(len(events.attendee_ids), 2, "It should have created one attendee per event")
+
+ def test_existing_attendee_added(self):
+ self.event.partner_ids = self.partner
+ attendee = self.event.attendee_ids
+ self.event.write({'partner_ids': [(4, self.partner.id)]}) # Add existing partner
+ self.assertEqual(self.event.attendee_ids, attendee, "It should not have created an new attendee record")
+
+ def test_attendee_add_self(self):
+ self.event.with_user(self.user).partner_ids = self.partner
+ self.assertTrue(self.event.attendee_ids, "It should have created an attendee")
+ self.assertEqual(self.event.attendee_ids.partner_id, self.partner, "It should be linked to the partner")
+ self.assertEqual(self.event.attendee_ids.state, 'accepted', "It should be accepted for the current user")
+
+ def test_attendee_removed(self):
+ partner_bis = self.env['res.partner'].create({'name': "Xavier"})
+ self.event.partner_ids = partner_bis
+ attendee = self.event.attendee_ids
+ self.event.partner_ids |= self.partner
+ self.event.partner_ids -= self.partner
+ self.assertEqual(attendee, self.event.attendee_ids, "It should not have re-created an attendee record")
+ self.assertNotIn(self.partner, self.event.attendee_ids.partner_id, "It should have removed the attendee")
+ self.assertNotIn(self.partner, self.event.message_follower_ids.partner_id, "It should have unsubscribed the partner")
+ self.assertIn(partner_bis, self.event.attendee_ids.partner_id, "It should have left the attendee")
diff --git a/addons/calendar/tests/test_calendar.py b/addons/calendar/tests/test_calendar.py
index 522a2738e94..8c8ca29d20d 100644
--- a/addons/calendar/tests/test_calendar.py
+++ b/addons/calendar/tests/test_calendar.py
@@ -273,11 +273,9 @@ class TestCalendar(SavepointCaseWithUserDemo):
def _test_one_mail_per_attendee(self, m, partners):
# check that every attendee receive a (single) mail for the event
for partner in partners:
- mail = self.env['mail.mail'].sudo().search([
- ('recipient_ids', 'in', partner.id),
- ('subject', 'like', m.name),
+ mail = self.env['mail.message'].sudo().search([
+ ('notified_partner_ids', 'in', partner.id),
])
- print(mail.mapped('subject'))
self.assertEqual(len(mail), 1)
partners = [
diff --git a/addons/calendar/tests/test_event_notifications.py b/addons/calendar/tests/test_event_notifications.py
new file mode 100644
index 00000000000..798ee73da3d
--- /dev/null
+++ b/addons/calendar/tests/test_event_notifications.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from unittest.mock import patch
+from datetime import datetime, date
+from dateutil.relativedelta import relativedelta
+
+from odoo import fields
+from odoo.tests.common import SavepointCase, new_test_user
+from odoo.addons.mail.tests.common import MailCase
+
+
+class TestEventNotifications(SavepointCase, MailCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.event = cls.env['calendar.event'].create({
+ 'name': "Doom's day",
+ 'start': datetime(2019, 10, 25, 8, 0),
+ 'stop': datetime(2019, 10, 27, 18, 0),
+ }).with_context(mail_notrack=True)
+ cls.user = new_test_user(cls.env, 'xav', email='em@il.com', notification_type='inbox')
+ cls.partner = cls.user.partner_id
+
+ def test_message_invite(self):
+ with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
+ 'message_type': 'user_notification',
+ 'subtype': 'mail.mt_note',
+ }):
+ self.event.partner_ids = self.partner
+
+ def test_message_invite_self(self):
+ with self.assertNoNotifications():
+ self.event.with_user(self.user).partner_ids = self.partner
+
+ def test_message_inactive_invite(self):
+ self.event.active = False
+ with self.assertNoNotifications():
+ self.event.partner_ids = self.partner
+
+ def test_message_set_inactive_invite(self):
+ self.event.active = False
+ with self.assertNoNotifications():
+ self.event.write({
+ 'partner_ids': [(4, self.partner.id)],
+ 'active': False,
+ })
+
+ def test_message_datetime_changed(self):
+ self.event.partner_ids = self.partner
+ "Invitation to Presentation of the new Calendar"
+ with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
+ 'message_type': 'user_notification',
+ 'subtype': 'mail.mt_note',
+ }):
+ self.event.start += relativedelta(days=-1)
+
+ def test_message_date_changed(self):
+ self.event.write({
+ 'allday': True,
+ 'start_date': date(2019, 10, 15),
+ 'stop_date': date(2019, 10, 15),
+ })
+ self.event.partner_ids = self.partner
+ with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
+ 'message_type': 'user_notification',
+ 'subtype': 'mail.mt_note',
+ }):
+ self.event.start_date += relativedelta(days=-1)
+
+ def test_message_set_inactive_date_changed(self):
+ self.event.write({
+ 'allday': True,
+ 'start_date': date(2019, 10, 15),
+ 'stop_date': date(2019, 10, 15),
+ })
+ self.event.partner_ids = self.partner
+ with self.assertNoNotifications():
+ self.event.write({
+ 'start_date': self.event.start_date - relativedelta(days=1),
+ 'active': False,
+ })
+
+ def test_message_inactive_date_changed(self):
+ self.event.write({
+ 'allday': True,
+ 'start_date': date(2019, 10, 15),
+ 'stop_date': date(2019, 10, 15),
+ 'active': False,
+ })
+ self.event.partner_ids = self.partner
+ with self.assertNoNotifications():
+ self.event.start_date += relativedelta(days=-1)
+
+ def test_message_add_and_date_changed(self):
+ self.event.partner_ids -= self.partner
+ with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
+ 'message_type': 'user_notification',
+ 'subtype': 'mail.mt_note',
+ }):
+ self.event.write({
+ 'start': self.event.start - relativedelta(days=1),
+ 'partner_ids': [(4, self.partner.id)],
+ })
+
+ def test_bus_notif(self):
+ alarm = self.env['calendar.alarm'].create({
+ 'name': 'Alarm',
+ 'alarm_type': 'notification',
+ 'interval': 'minutes',
+ 'duration': 30,
+ })
+ now = fields.Datetime.now()
+ with patch.object(fields.Datetime, 'now', lambda: now):
+ with self.assertBus([(self.env.cr.dbname, 'calendar.alarm', self.partner.id)]):
+ self.event.with_context(no_mail_to_attendees=True).write({
+ 'start': now + relativedelta(minutes=50),
+ 'stop': now + relativedelta(minutes=55),
+ 'partner_ids': [(4, self.partner.id)],
+ 'alarm_ids': [(4, alarm.id)]
+ })
+ bus_message = [{
+ "alarm_id": alarm.id,
+ "event_id": self.event.id,
+ "title": "Doom's day",
+ "message": self.event.display_time,
+ "timer": 20*60,
+ "notify_at": fields.Datetime.to_string(now + relativedelta(minutes=20)),
+ }]
+ notif = self.env['calendar.alarm_manager'].with_user(self.user).get_next_notif()
+ self.assertEqual(notif, bus_message)
+
+ def test_email_alarm(self):
+ alarm = self.env['calendar.alarm'].create({
+ 'name': 'Alarm',
+ 'alarm_type': 'email',
+ 'interval': 'minutes',
+ 'duration': 20,
+ })
+ now = fields.Datetime.now()
+ self.event.write({
+ 'start': now + relativedelta(minutes=15),
+ 'stop': now + relativedelta(minutes=18),
+ 'partner_ids': [(4, self.partner.id)],
+ 'alarm_ids': [(4, alarm.id)],
+ })
+ with patch.object(fields.Datetime, 'now', lambda: now):
+ with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
+ 'message_type': 'user_notification',
+ 'subtype': 'mail.mt_note',
+ }):
+ self.env['calendar.alarm_manager'].with_context(lastcall=now - relativedelta(minutes=15))._get_partner_next_mail(self.partner)
diff --git a/addons/calendar/tests/test_event_recurrence.py b/addons/calendar/tests/test_event_recurrence.py
index e9b8ec5447e..e4519be71c0 100644
--- a/addons/calendar/tests/test_event_recurrence.py
+++ b/addons/calendar/tests/test_event_recurrence.py
@@ -584,7 +584,6 @@ class TestUpdateMonthlyByDay(TestRecurrentEvents):
'stop': event.stop - relativedelta(days=4),
})
- # LUL TODO test shift future
class TestUpdateMonthlyByDate(TestRecurrentEvents):
|
|