[MERGE] calendar: improve wording of calendar mail templates

Purpose of this merge is to make templates easier to preview and edit as
weel as to improve their content.

This is achieved notably by lessening use of custom rendering context and
trying to fix as much error-prone jinja statements.

In this commit we also make ``calendar_template_meeting_reminder`` template
looking more like other templates. There were some rendering differences that
do not seem really wanted.

``calendar_template_meeting_reminder`` was also using an old ``force_event_id``
key not replaced by recurrent_id field on attendee.

This merge improves few strings of the invitation mail being sent while
booking an online appointment. Online appointment is computed based on
``appointment_type_id`` field being defined. Some tweaks in wording made it
necessary to know if recipient is the responsible, customer or an added
attendee on the event, leading to some tooling code added on event model.

The main changes includes

 * for customer, remove the CTAs to accept / decline event as attendee are
   automatically set as accepted;
 * depending on recipient, improve wording of the mail accordingly and mention
   appointment type in the mail;

Some code reodering and cleaning is also performed in this merge to ease
reading and understanding of calendar event and attendee models.

LINKS

Task ID-2199620
COM PR #64925
ENT PR odoo/enterprise#15914
UPG PR odoo/upgrade#

Related: odoo/upgrade#2282
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2021-03-25 08:55:53 +01:00
committed by GitHub
7 changed files with 683 additions and 548 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
{
'name': 'Calendar',
'version': '1.0',
'version': '1.1',
'sequence': 165,
'depends': ['base', 'mail'],
'summary': "Schedule employees' meetings",
+105 -38
View File
@@ -11,26 +11,46 @@
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="html">
<div>
% set colors = ctx.get('colors', {})
% set recurrent = object.recurrence_id and not ctx['ignore_recurrence']
% set colors = {'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}
% set is_online = 'appointment_type_id' in object.event_id and object.event_id.appointment_type_id
% set customer = object.event_id.find_partner_customer()
% set target_responsible = object.partner_id == object.event_id.partner_id
% set target_customer = object.partner_id == customer
<p>
Hello ${object.common_name},<br/><br/>
${object.event_id.user_id.partner_id.name} invited you to the ${object.event_id.name} meeting of ${object.event_id.user_id.company_id.name}.
% if is_online and target_customer:
Your appointment <strong>${object.event_id.appointment_type_id.name}</strong> with ${object.event_id.user_id.name} has been booked.
% elif is_online and target_responsible:
% if customer
${customer.name} scheduled the following appointment <strong>${object.event_id.appointment_type_id.name}</strong> with you.
% else
Your appointment <strong>${object.event_id.appointment_type_id.name}</strong> has been booked.
% endif
% elif not target_responsible:
${object.event_id.user_id.partner_id.name} invited you for the <strong>${object.event_id.name}</strong> meeting.
% else:
Your meeting <strong>${object.event_id.name}</strong> has been booked.
% endif
</p>
<div style="text-align: center; margin: 16px 0px 16px 0px;">
% set target = 'recurrence' if recurrent else 'meeting'
<a href="/calendar/${target}/accept?token=${object.access_token}&amp;id=${object.event_id.id}"
% if not is_online or object.state != 'accepted':
<a href="/calendar/meeting/accept?token=${object.access_token}&amp;id=${object.event_id.id}"
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
Accept</a>
<a href="/calendar/${target}/decline?token=${object.access_token}&amp;id=${object.event_id.id}"
<a href="/calendar/meeting/decline?token=${object.access_token}&amp;id=${object.event_id.id}"
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
Decline</a>
% endif
<a href="/calendar/meeting/view?token=${object.access_token}&amp;id=${object.event_id.id}"
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
View</a>
${'Reschedule' if is_online and target_customer else 'View'}
</a>
</div>
<table border="0" cellpadding="0" cellspacing="0"><tr>
% if not recurrent:
<td width="130px;">
<div style="border-top-left-radius: 3px; border-top-right-radius: 3px; font-size: 12px; border-collapse: separate; text-align: center; font-weight: bold; color: #ffffff; min-height: 18px; background-color: #875A7B; border: 1px solid #875A7B;">
${object.event_id.get_interval('dayname', tz=object.partner_id.tz if not object.event_id.allday else None)}
@@ -46,24 +66,35 @@
</div>
</td>
<td width="20px;"/>
% endif
<td style="padding-top: 5px;">
<p><strong>Details of the event</strong></p>
<ul>
% if is_online:
<li>Appointment Type: ${object.event_id.appointment_type_id.name}</li>
% endif
% if object.event_id.location:
<li>Location: ${object.event_id.location}
(<a target="_blank" href="http://maps.google.com/maps?oi=map&amp;q=${object.event_id.location}">View Map</a>)
</li>
% endif
% if object.event_id.description :
<li>Description: ${object.event_id.description}</li>
% endif
% if recurrent:
<li>When: ${object.recurrence_id.name}</li>
% endif
% if not object.event_id.allday and object.event_id.duration
<li>Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}</li>
% endif
% if not is_online and object.event_id.description :
<li>Description: ${object.event_id.description}</li>
% elif is_online and object.event_id.description:
% set splitted_description = object.event_id.description_to_html_lines()
<li>Description:
<ul>
% for description_line in splitted_description:
<li>${description_line | safe}</li>
% endfor
</ul>
</li>
% endif
<li>Attendees
<ul>
% for attendee in object.event_id.attendee_ids:
@@ -105,18 +136,38 @@
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="html">
<div>
% set colors = ctx.get('colors', {})
% set recurrent = object.recurrence_id and not ctx['ignore_recurrence']
% set colors = {'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}
% set is_online = 'appointment_type_id' in object.event_id and object.event_id.appointment_type_id
% set customer = object.event_id.find_partner_customer()
% set target_responsible = object.partner_id == object.event_id.partner_id
% set target_customer = object.partner_id == customer
<p>
Hello ${object.common_name},<br/><br/>
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 is_online and target_responsible:
% if customer:
The date of your appointment with ${customer.name} has been updated.
% else
Your appointment has been updated.
% endif
The appointment <strong>${object.event_id.appointment_type_id.name}</strong> is now scheduled for
${object.event_id.get_display_time_tz(tz=object.partner_id.tz)}.
% elif is_online and target_customer:
The date of your appointment with ${object.event_id.user_id.partner_id.name} has been updated.
The appointment <strong>${object.event_id.appointment_type_id.name}</strong> is now scheduled for
${object.event_id.get_display_time_tz(tz=object.partner_id.tz)}.
% else:
The date of the meeting has been updated.
The meeting <strong>${object.event_id.name}</strong> 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)}.
% endif
</p>
<div style="text-align: center; margin: 16px 0px 16px 0px;">
% set target = 'recurrence' if recurrent else 'meeting'
<a href="/calendar/${target}/accept?token=${object.access_token}&amp;id=${object.event_id.id}"
<a href="/calendar/meeting/accept?token=${object.access_token}&amp;id=${object.event_id.id}"
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
Accept</a>
<a href="/calendar/${target}/decline?token=${object.access_token}&amp;id=${object.event_id.id}"
<a href="/calendar/meeting/decline?token=${object.access_token}&amp;id=${object.event_id.id}"
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
Decline</a>
<a href="/calendar/meeting/view?token=${object.access_token}&amp;id=${object.event_id.id}"
@@ -124,7 +175,6 @@
View</a>
</div>
<table border="0" cellpadding="0" cellspacing="0"><tr>
% if not recurrent:
<td width="130px;">
<div style="border-top-left-radius: 3px; border-top-right-radius: 3px; font-size: 12px; border-collapse: separate; text-align: center; font-weight: bold; color: #ffffff; min-height: 18px; background-color: #875A7B; border: 1px solid #875A7B;">
${object.event_id.get_interval('dayname', tz=object.partner_id.tz if not object.event_id.allday else None)}
@@ -140,7 +190,6 @@
</div>
</td>
<td width="20px;"/>
% endif
<td style="padding-top: 5px;">
<p><strong>Details of the event</strong></p>
<ul>
@@ -149,15 +198,24 @@
(<a target="_blank" href="http://maps.google.com/maps?oi=map&amp;q=${object.event_id.location}">View Map</a>)
</li>
% endif
% if object.event_id.description :
<li>Description: ${object.event_id.description}</li>
% endif
% if recurrent:
<li>When: ${object.recurrence_id.name}</li>
% endif
% if not object.event_id.allday and object.event_id.duration
<li>Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}</li>
% endif
% if not is_online and object.event_id.description :
<li>Description: ${object.event_id.description}</li>
% elif is_online and object.event_id.description:
% set splitted_description = object.event_id.description_to_html_lines()
<li>Description:
<ul>
% for description_line in splitted_description:
<li>${description_line | safe}</li>
% endfor
</ul>
</li>
% endif
<li>Attendees
<ul>
% for attendee in object.event_id.attendee_ids:
@@ -199,21 +257,18 @@
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="html">
<div>
% set colors = {'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}
<!--
In a recurring event case, the object.event_id is always the first event
This makes the event date (and a lot of other information) incorrect
-->
% set event_id = ctx.get('force_event_id') or object.event_id
% set colors = {'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}
% set is_online = 'appointment_type_id' in object.event_id and object.event_id.appointment_type_id
% set recurrent = object.recurrence_id and not ctx.get('calendar_template_ignore_recurrence')
<p>
Hello ${object.common_name},<br/><br/>
This is a reminder for the below event :
</p>
<div style="text-align: center; margin: 16px 0px 16px 0px;">
<a href="/calendar/meeting/accept?token=${object.access_token}&amp;id=${object.event_id.id}"
<a href="/calendar/${'recurrence' if recurrent else 'meeting'}/accept?token=${object.access_token}&amp;id=${object.event_id.id}"
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
Accept</a>
<a href="/calendar/meeting/decline?token=${object.access_token}&amp;id=${object.event_id.id}"
<a href="/calendar/${'recurrence' if recurrent else 'meeting'}/decline?token=${object.access_token}&amp;id=${object.event_id.id}"
style="padding: 5px 10px; color: #FFFFFF; text-decoration: none; background-color: #875A7B; border: 1px solid #875A7B; border-radius: 3px">
Decline</a>
<a href="/calendar/meeting/view?token=${object.access_token}&amp;id=${object.event_id.id}"
@@ -223,16 +278,16 @@
<table border="0" cellpadding="0" cellspacing="0"><tr>
<td width="130px;">
<div style="border-top-left-radius: 3px; border-top-right-radius: 3px; font-size: 12px; border-collapse: separate; text-align: center; font-weight: bold; color: #ffffff; min-height: 18px; background-color: #875A7B; border: 1px solid #875A7B;">
${event_id.get_interval('dayname', tz=object.partner_id.tz if not event_id.allday else None)}
${object.event_id.get_interval('dayname', tz=object.partner_id.tz if not object.event_id.allday else None)}
</div>
<div style="font-size: 48px; min-height: auto; font-weight: bold; text-align: center; color: #5F5F5F; background-color: #F8F8F8; border: 1px solid #875A7B;">
${event_id.get_interval('day', tz=object.partner_id.tz if not event_id.allday else None)}
${object.event_id.get_interval('day', tz=object.partner_id.tz if not object.event_id.allday else None)}
</div>
<div style='font-size: 12px; text-align: center; font-weight: bold; color: #ffffff; background-color: #875A7B;'>
${event_id.get_interval('month', tz=object.partner_id.tz if not event_id.allday else None)}
${object.event_id.get_interval('month', tz=object.partner_id.tz if not object.event_id.allday else None)}
</div>
<div style="border-collapse: separate; color: #5F5F5F; text-align: center; font-size: 12px; border-bottom-right-radius: 3px; font-weight: bold; border: 1px solid #875A7B; border-bottom-left-radius: 3px;">
${not event_id.allday and event_id.get_interval('time', tz=object.partner_id.tz) or ''}
${not object.event_id.allday and object.event_id.get_interval('time', tz=object.partner_id.tz) or ''}
</div>
</td>
<td width="20px;"/>
@@ -244,11 +299,23 @@
(<a target="_blank" href="http://maps.google.com/maps?oi=map&amp;q=${object.event_id.location}">View Map</a>)
</li>
% endif
% if object.event_id.description :
<li>Description: ${object.event_id.description}</li>
% if recurrent:
<li>When: ${object.recurrence_id.name}</li>
% endif
% if not object.event_id.allday and object.event_id.duration
<li>Duration: ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}</li>
<li>Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}</li>
% endif
% if not is_online and object.event_id.description :
<li>Description: ${object.event_id.description}</li>
% elif is_online and object.event_id.description:
% set splitted_description = object.event_id.description_to_html_lines()
<li>Description:
<ul>
% for description_line in splitted_description:
<li>${description_line | safe}</li>
% endfor
</ul>
</li>
% endif
<li>Attendees
<ul>
@@ -167,10 +167,9 @@ class AlarmManager(models.AbstractModel):
# Executed via cron
events = self._get_events_to_notify('email')
attendees = events.attendee_ids.filtered(lambda a: a.state != 'declined')
attendees._send_mail_to_attendees(
attendees.with_context(calendar_template_ignore_recurrence=True)._send_mail_to_attendees(
'calendar.calendar_template_meeting_reminder',
force_send=True,
ignore_recurrence=True,
)
@api.model
+16 -32
View File
@@ -15,6 +15,7 @@ class Attendee(models.Model):
_name = 'calendar.attendee'
_rec_name = 'common_name'
_description = 'Calendar Attendee Information'
_order = 'create_date ASC'
def _default_access_token(self):
return uuid.uuid4().hex
@@ -26,18 +27,20 @@ class Attendee(models.Model):
('accepted', 'Accepted'),
]
event_id = fields.Many2one(
'calendar.event', 'Meeting linked', required=True, ondelete='cascade')
# event
event_id = fields.Many2one('calendar.event', 'Meeting linked', required=True, ondelete='cascade')
recurrence_id = fields.Many2one('calendar.recurrence', related='event_id.recurrence_id')
# attendee
partner_id = fields.Many2one('res.partner', 'Attendee', 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)
email = fields.Char('Email', related='partner_id.email', help="Email of Invited Person")
phone = fields.Char('Phone', related='partner_id.phone', help="Phone number of Invited Person")
common_name = fields.Char('Common name', compute='_compute_common_name', store=True)
access_token = fields.Char('Invitation Token', default=_default_access_token)
# state
state = fields.Selection(STATE_SELECTION, string='Status', readonly=True, default='needsAction',
help="Status of the attendee's participation")
availability = fields.Selection(
[('free', 'Available'), ('busy', 'Busy')], 'Available/Busy', readonly=True)
access_token = fields.Char('Invitation Token', default=_default_access_token)
recurrence_id = fields.Many2one('calendar.recurrence', related='event_id.recurrence_id')
@api.depends('partner_id', 'partner_id.name', 'email')
def _compute_common_name(self):
@@ -62,6 +65,10 @@ class Attendee(models.Model):
self._unsubscribe_partner()
return super().unlink()
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
raise UserError(_('You cannot duplicate a calendar attendee.'))
def _subscribe_partner(self):
for event in self.event_id:
partners = (event.attendee_ids & self).partner_id - event.message_partner_ids
@@ -74,22 +81,16 @@ class Attendee(models.Model):
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, ignore_recurrence=False):
def _send_mail_to_attendees(self, template_xmlid, force_send=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
if self.env['ir.config_parameter'].sudo().get_param('calendar.block_mail') or self._context.get("no_mail_to_attendees"):
return res
calendar_view = self.env.ref('calendar.view_calendar_event_calendar')
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))
@@ -97,25 +98,8 @@ class Attendee(models.Model):
# get ics file for all meetings
ics_files = self.mapped('event_id')._get_ics_file()
# prepare rendering context for mail template
colors = {
'needsAction': 'grey',
'accepted': 'green',
'tentative': '#FFFF00',
'declined': 'red'
}
rendering_context = dict(self._context)
rendering_context.update({
'colors': colors,
'ignore_recurrence': ignore_recurrence,
'action_id': self.env['ir.actions.act_window'].sudo().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'),
})
for attendee in self:
if attendee.email and attendee.partner_id != self.env.user.partner_id:
# FIXME: is ics_file text or bytes?
event_id = attendee.event_id.id
ics_file = ics_files.get(event_id)
@@ -126,7 +110,7 @@ class Attendee(models.Model):
'mimetype': 'text/calendar',
'datas': base64.b64encode(ics_file)})
]
body = invitation_template.with_context(rendering_context)._render_field(
body = invitation_template._render_field(
'body_html',
attendee.ids,
compute_lang=True,
File diff suppressed because it is too large Load Diff
@@ -9,11 +9,11 @@ class Attendee(models.Model):
_name = 'calendar.attendee'
_inherit = 'calendar.attendee'
def _send_mail_to_attendees(self, template_xmlid, force_send=False, ignore_recurrence=False):
def _send_mail_to_attendees(self, template_xmlid, force_send=False):
""" Override
If not synced with Google, let Odoo in charge of sending emails
Otherwise, nothing to do: Google will send them
"""
with google_calendar_token(self.env.user.sudo()) as token:
if not token:
super()._send_mail_to_attendees(template_xmlid, force_send, ignore_recurrence)
super()._send_mail_to_attendees(template_xmlid, force_send)
@@ -10,11 +10,11 @@ class Attendee(models.Model):
_name = 'calendar.attendee'
_inherit = 'calendar.attendee'
def _send_mail_to_attendees(self, template_xmlid, force_send=False, ignore_recurrence=False):
def _send_mail_to_attendees(self, template_xmlid, force_send=False):
""" Override the super method
If not synced with Microsoft Outlook, let Odoo in charge of sending emails
Otherwise, Microsoft Outlook will send them
"""
with microsoft_calendar_token(self.env.user.sudo()) as token:
if not token:
super()._send_mail_to_attendees(template_xmlid, force_send, ignore_recurrence)
super()._send_mail_to_attendees(template_xmlid, force_send)