- % if not recurrent:
|
${object.event_id.get_interval('dayname', tz=object.partner_id.tz if not object.event_id.allday else None)}
@@ -46,24 +66,35 @@
|
|
- % endif
Details of the event
+ % if is_online:
+ - Appointment Type: ${object.event_id.appointment_type_id.name}
+ % endif
% if object.event_id.location:
- Location: ${object.event_id.location}
(View Map)
% endif
- % 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
+ % if not is_online and object.event_id.description :
+ - Description: ${object.event_id.description}
+ % elif is_online and object.event_id.description:
+ % set splitted_description = object.event_id.description_to_html_lines()
+ - Description:
+
+ % for description_line in splitted_description:
+ - ${description_line | safe}
+ % endfor
+
+
+ % endif
- Attendees
% for attendee in object.event_id.attendee_ids:
@@ -105,18 +136,38 @@
${object.partner_id.lang}
- % 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
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 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 ${object.event_id.appointment_type_id.name} 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 ${object.event_id.appointment_type_id.name} 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 ${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)}.
+ % endif
- % set target = 'recurrence' if recurrent else 'meeting'
-
Accept
-
Decline
- % if not recurrent:
|
${object.event_id.get_interval('dayname', tz=object.partner_id.tz if not object.event_id.allday else None)}
@@ -140,7 +190,6 @@
|
|
- % endif
Details of the event
@@ -149,15 +198,24 @@
(View Map)
% endif
- % 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
+ % if not is_online and object.event_id.description :
+ - Description: ${object.event_id.description}
+ % elif is_online and object.event_id.description:
+ % set splitted_description = object.event_id.description_to_html_lines()
+ - Description:
+
+ % for description_line in splitted_description:
+ - ${description_line | safe}
+ % endfor
+
+
+ % endif
- Attendees
% for attendee in object.event_id.attendee_ids:
@@ -199,21 +257,18 @@
${object.partner_id.lang}
- % set colors = {'needsAction': 'grey', 'accepted': 'green', 'tentative': '#FFFF00', 'declined': 'red'}
-
- % 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')
Hello ${object.common_name},
This is a reminder for the below event :
-
Accept
-
Decline
|
- ${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)}
- ${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)}
- ${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)}
- ${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 ''}
|
|
@@ -244,11 +299,23 @@
(View Map)
% endif
- % if object.event_id.description :
- - Description: ${object.event_id.description}
+ % 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,(object.event_id.duration*60)%60))}
+ - Duration: ${('%dH%02d' % (object.event_id.duration,round(object.event_id.duration*60)%60))}
+ % endif
+ % if not is_online and object.event_id.description :
+ - Description: ${object.event_id.description}
+ % elif is_online and object.event_id.description:
+ % set splitted_description = object.event_id.description_to_html_lines()
+ - Description:
+
+ % for description_line in splitted_description:
+ - ${description_line | safe}
+ % endfor
+
+
% endif
- Attendees
diff --git a/addons/calendar/models/calendar_alarm_manager.py b/addons/calendar/models/calendar_alarm_manager.py
index 9855ec0af51..6450690f1d2 100644
--- a/addons/calendar/models/calendar_alarm_manager.py
+++ b/addons/calendar/models/calendar_alarm_manager.py
@@ -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
diff --git a/addons/calendar/models/calendar_attendee.py b/addons/calendar/models/calendar_attendee.py
index ebcb8511de4..50232d0a6b7 100644
--- a/addons/calendar/models/calendar_attendee.py
+++ b/addons/calendar/models/calendar_attendee.py
@@ -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,
diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py
index 500241de5fd..4570465ffce 100644
--- a/addons/calendar/models/calendar_event.py
+++ b/addons/calendar/models/calendar_event.py
@@ -16,11 +16,17 @@ from odoo.addons.calendar.models.calendar_attendee import Attendee
from odoo.addons.calendar.models.calendar_recurrence import weekday_to_field, RRULE_TYPE_SELECTION, END_TYPE_SELECTION, MONTH_BY_SELECTION, WEEKDAY_SELECTION, BYDAY_SELECTION
from odoo.tools.translate import _
from odoo.tools.misc import get_lang
-from odoo.tools import pycompat
+from odoo.tools import pycompat, html_escape
from odoo.exceptions import UserError, ValidationError, AccessError
_logger = logging.getLogger(__name__)
+try:
+ import vobject
+except ImportError:
+ _logger.warning("`vobject` Python module not found, iCal file generation disabled. Consider installing this module if you want to generate iCal files")
+ vobject = None
+
SORT_ALIASES = {
'start': 'sort_start',
'start_date': 'sort_start',
@@ -89,141 +95,21 @@ class Meeting(models.Model):
jitsi_url = 'https://' + jitsi_url
return url_join(jitsi_url, 'odoo-%s' % (uuid4().hex[:12]))
- def _find_my_attendee(self):
- """ Return the first attendee where the user connected has been invited
- from all the meeting_ids in parameters.
- """
- self.ensure_one()
- for attendee in self.attendee_ids:
- if self.env.user.partner_id == attendee.partner_id:
- return attendee
- return False
-
- @api.model
- def _get_date_formats(self):
- """ get current date and time format, according to the context lang
- :return: a tuple with (format date, format time)
- """
- lang = get_lang(self.env)
- return (lang.date_format, lang.time_format)
-
- @api.model
- def _get_recurrent_fields(self):
- return {'byday', 'until', 'rrule_type', 'month_by', 'event_tz', 'rrule',
- 'interval', 'count', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa',
- 'su', 'day', 'weekday'}
-
- @api.model
- def _get_time_fields(self):
- return {'start', 'stop', 'start_date', 'stop_date'}
-
- @api.model
- def _get_custom_fields(self):
- all_fields = self.fields_get(attributes=['manual'])
- return {fname for fname in all_fields if all_fields[fname]['manual']}
-
- @api.model
- def _get_public_fields(self):
- return self._get_recurrent_fields() | self._get_time_fields() | self._get_custom_fields() | {
- 'id', 'active', 'allday',
- 'duration', 'user_id', 'interval',
- 'count', 'rrule', 'recurrence_id', 'show_as', 'privacy'}
-
- @api.model
- def _get_display_time(self, start, stop, zduration, zallday):
- """ Return date and time (from to from) based on duration with timezone in string. Eg :
- 1) if user add duration for 2 hours, return : August-23-2013 at (04-30 To 06-30) (Europe/Brussels)
- 2) if event all day ,return : AllDay, July-31-2013
- """
- timezone = self._context.get('tz') or self.env.user.partner_id.tz or 'UTC'
-
- # get date/time format according to context
- format_date, format_time = self._get_date_formats()
-
- # convert date and time into user timezone
- self_tz = self.with_context(tz=timezone)
- date = fields.Datetime.context_timestamp(self_tz, fields.Datetime.from_string(start))
- date_deadline = fields.Datetime.context_timestamp(self_tz, fields.Datetime.from_string(stop))
-
- # convert into string the date and time, using user formats
- to_text = pycompat.to_text
- date_str = to_text(date.strftime(format_date))
- time_str = to_text(date.strftime(format_time))
-
- if zallday:
- display_time = _("All Day, %(day)s", day=date_str)
- elif zduration < 24:
- duration = date + timedelta(minutes=round(zduration*60))
- duration_time = to_text(duration.strftime(format_time))
- display_time = _(
- u"%(day)s at (%(start)s To %(end)s) (%(timezone)s)",
- day=date_str,
- start=time_str,
- end=duration_time,
- timezone=timezone,
- )
- else:
- dd_date = to_text(date_deadline.strftime(format_date))
- dd_time = to_text(date_deadline.strftime(format_time))
- display_time = _(
- u"%(date_start)s at %(time_start)s To\n %(date_end)s at %(time_end)s (%(timezone)s)",
- date_start=date_str,
- time_start=time_str,
- date_end=dd_date,
- time_end=dd_time,
- timezone=timezone,
- )
- return display_time
-
- def _get_duration(self, start, stop):
- """ Get the duration value between the 2 given dates. """
- if not start or not stop:
- return 0
- duration = (stop - start).total_seconds() / 3600
- return round(duration, 2)
-
- def _compute_is_highlighted(self):
- if self.env.context.get('active_model') == 'res.partner':
- partner_id = self.env.context.get('active_id')
- for event in self:
- if event.partner_ids.filtered(lambda s: s.id == partner_id):
- event.is_highlighted = True
- else:
- event.is_highlighted = False
- else:
- for event in self:
- event.is_highlighted = False
-
+ # description
name = fields.Char('Meeting Subject', required=True)
-
- attendee_status = fields.Selection(
- Attendee.STATE_SELECTION, string='Attendee Status', compute='_compute_attendee')
- display_time = fields.Char('Event Time', compute='_compute_display_time')
- start = fields.Datetime(
- 'Start', required=True, tracking=True, default=fields.Date.today,
- help="Start date of an event, without time for full days events")
- stop = fields.Datetime(
- 'Stop', required=True, tracking=True, default=lambda self: fields.Datetime.today() + timedelta(hours=1),
- compute='_compute_stop', readonly=False, store=True,
- help="Stop date of an event, without time for full days events")
-
- allday = fields.Boolean('All Day', default=False)
- start_date = fields.Date(
- 'Start Date', store=True, tracking=True,
- compute='_compute_dates', inverse='_inverse_dates')
- stop_date = fields.Date(
- 'End Date', store=True, tracking=True,
- compute='_compute_dates', inverse='_inverse_dates')
- duration = fields.Float('Duration', compute='_compute_duration', store=True, readonly=False)
description = fields.Text('Description')
+ user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self.env.user)
+ partner_id = fields.Many2one(
+ 'res.partner', string='Responsible Contact', related='user_id.partner_id', readonly=True)
+ location = fields.Char('Location', tracking=True, help="Location of Event")
+ videocall_location = fields.Char('Join Video Call', default=_default_videocall_location)
+ # visibility
privacy = fields.Selection(
[('public', 'Public'),
('private', 'Private'),
('confidential', 'Only internal users')],
'Privacy', default='public', required=True,
help="People to whom this event will be visible.")
- location = fields.Char('Location', tracking=True, help="Location of Event")
- videocall_location = fields.Char('Join Video Call', default=_default_videocall_location)
show_as = fields.Selection(
[('free', 'Available'),
('busy', 'Busy')], 'Show as', default='busy', required=True,
@@ -232,38 +118,54 @@ class Meeting(models.Model):
that you are unavailable during that period of time. \n If the time is shown as 'free', this event won't \
be visible to other people at all. Use this option to let other people know that you are available during \
that period of time.")
-
+ is_highlighted = fields.Boolean(
+ compute='_compute_is_highlighted', string='Is the Event Highlighted')
+ # filtering
+ active = fields.Boolean(
+ 'Active', default=True,
+ help="If the active field is set to false, it will allow you to hide the event alarm information without removing it.")
+ categ_ids = fields.Many2many(
+ 'calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags')
+ # timing
+ start = fields.Datetime(
+ 'Start', required=True, tracking=True, default=fields.Date.today,
+ help="Start date of an event, without time for full days events")
+ stop = fields.Datetime(
+ 'Stop', required=True, tracking=True, default=lambda self: fields.Datetime.today() + timedelta(hours=1),
+ compute='_compute_stop', readonly=False, store=True,
+ help="Stop date of an event, without time for full days events")
+ display_time = fields.Char('Event Time', compute='_compute_display_time')
+ allday = fields.Boolean('All Day', default=False)
+ start_date = fields.Date(
+ 'Start Date', store=True, tracking=True,
+ compute='_compute_dates', inverse='_inverse_dates')
+ stop_date = fields.Date(
+ 'End Date', store=True, tracking=True,
+ compute='_compute_dates', inverse='_inverse_dates')
+ duration = fields.Float('Duration', compute='_compute_duration', store=True, readonly=False)
# linked document
# LUL TODO use fields.Reference ?
res_id = fields.Integer('Document ID')
res_model_id = fields.Many2one('ir.model', 'Document Model', ondelete='cascade')
res_model = fields.Char(
'Document Model Name', related='res_model_id.model', readonly=True, store=True)
+ # messaging
activity_ids = fields.One2many('mail.activity', 'calendar_event_id', string='Activities')
-
#redifine message_ids to remove autojoin to avoid search to crash in get_recurrent_ids
message_ids = fields.One2many(auto_join=False)
-
- user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self.env.user)
- partner_id = fields.Many2one(
- 'res.partner', string='Responsible Contact', related='user_id.partner_id', readonly=True)
- active = fields.Boolean(
- 'Active', default=True,
- help="If the active field is set to false, it will allow you to hide the event alarm information without removing it.")
- categ_ids = fields.Many2many(
- 'calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags')
+ # attendees
attendee_ids = fields.One2many(
'calendar.attendee', 'event_id', 'Participant')
+ attendee_status = fields.Selection(
+ Attendee.STATE_SELECTION, string='Attendee Status', compute='_compute_attendee')
partner_ids = fields.Many2many(
'res.partner', 'calendar_event_res_partner_rel',
string='Attendees', default=_default_partners)
+ # alarms
alarm_ids = fields.Many2many(
'calendar.alarm', 'calendar_alarm_calendar_event_rel',
string='Reminders', ondelete="restrict",
help="Notifications sent to all attendees to remind of the meeting.")
- is_highlighted = fields.Boolean(
- compute='_compute_is_highlighted', string='Is the Event Highlighted')
-
# RECURRENCE FIELD
recurrency = fields.Boolean('Recurrent')
recurrence_id = fields.Many2one(
@@ -275,7 +177,6 @@ class Meeting(models.Model):
('all_events', "All events"),
], store=False, copy=False, default='self_only',
help="Choose what to do with other events in the recurrence. Updating All Events is not allowed when dates or time is modified")
-
# Those field are pseudo-related fields of recurrence_id.
# They can't be "real" related fields because it should work at record creation
# when recurrence_id is not created yet.
@@ -309,10 +210,17 @@ class Meeting(models.Model):
byday = fields.Selection(BYDAY_SELECTION, compute='_compute_recurrence', readonly=False)
until = fields.Date(compute='_compute_recurrence', readonly=False)
- def _compute_attendee(self):
- for meeting in self:
- attendee = meeting._find_my_attendee()
- meeting.attendee_status = attendee.state if attendee else 'needsAction'
+ def _compute_is_highlighted(self):
+ if self.env.context.get('active_model') == 'res.partner':
+ partner_id = self.env.context.get('active_id')
+ for event in self:
+ if event.partner_ids.filtered(lambda s: s.id == partner_id):
+ event.is_highlighted = True
+ else:
+ event.is_highlighted = False
+ else:
+ for event in self:
+ event.is_highlighted = False
def _compute_display_time(self):
for meeting in self:
@@ -377,6 +285,15 @@ class Meeting(models.Model):
'stop': enddate.replace(tzinfo=None)
})
+ def _compute_attendee(self):
+ for meeting in self:
+ attendee = next(
+ (attendee for attendee in self.attendee_ids
+ if attendee.partner_id == self.env.user.partner_id),
+ self.env['calendar.attendee']
+ )
+ meeting.attendee_status = attendee.state if attendee else 'needsAction'
+
@api.constrains('start', 'stop', 'start_date', 'stop_date')
def _check_closing_date(self):
for meeting in self:
@@ -399,10 +316,6 @@ class Meeting(models.Model):
)
)
- ####################################################
- # Calendar Business, Reccurency, ...
- ####################################################
-
@api.depends('recurrence_id', 'recurrency')
def _compute_recurrence(self):
recurrence_fields = self._get_recurrent_fields()
@@ -420,321 +333,9 @@ class Meeting(models.Model):
else:
event.update(false_values)
- def _get_ics_file(self):
- """ Returns iCalendar file for the event invitation.
- :returns a dict of .ics file content for each meeting
- """
- result = {}
-
- def ics_datetime(idate, allday=False):
- if idate:
- if allday:
- return idate
- return idate.replace(tzinfo=pytz.timezone('UTC'))
- return False
-
- try:
- # FIXME: why isn't this in CalDAV?
- import vobject
- except ImportError:
- _logger.warning("The `vobject` Python module is not installed, so iCal file generation is unavailable. Please install the `vobject` Python module")
- return result
-
- for meeting in self:
- cal = vobject.iCalendar()
- event = cal.add('vevent')
-
- if not meeting.start or not meeting.stop:
- raise UserError(_("First you have to specify the date of the invitation."))
- event.add('created').value = ics_datetime(fields.Datetime.now())
- event.add('dtstart').value = ics_datetime(meeting.start, meeting.allday)
- event.add('dtend').value = ics_datetime(meeting.stop, meeting.allday)
- event.add('summary').value = meeting.name
- if meeting.description:
- event.add('description').value = meeting.description
- if meeting.location:
- event.add('location').value = meeting.location
- if meeting.rrule:
- event.add('rrule').value = meeting.rrule
-
- if meeting.alarm_ids:
- for alarm in meeting.alarm_ids:
- valarm = event.add('valarm')
- interval = alarm.interval
- duration = alarm.duration
- trigger = valarm.add('TRIGGER')
- trigger.params['related'] = ["START"]
- if interval == 'days':
- delta = timedelta(days=duration)
- elif interval == 'hours':
- delta = timedelta(hours=duration)
- elif interval == 'minutes':
- delta = timedelta(minutes=duration)
- trigger.value = delta
- valarm.add('DESCRIPTION').value = alarm.name or u'Odoo'
- for attendee in meeting.attendee_ids:
- attendee_add = event.add('attendee')
- attendee_add.value = u'MAILTO:' + (attendee.email or u'')
- result[meeting.id] = cal.serialize().encode('utf-8')
-
- return result
-
- 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 = []
-
- 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
-
- 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_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
- :param string interval: Among 'day', 'month', 'dayname' and 'time' indicating the desired formatting
- :param string tz: Timezone indicator (optional)
- :return unicode: Formatted date or time (as unicode string, to prevent jinja2 crash)
- """
- self.ensure_one()
- date = fields.Datetime.from_string(self.start)
-
- if tz:
- timezone = pytz.timezone(tz or 'UTC')
- date = date.replace(tzinfo=pytz.timezone('UTC')).astimezone(timezone)
-
- if interval == 'day':
- # Day number (1-31)
- result = str(date.day)
-
- elif interval == 'month':
- # Localized month name and year
- result = babel.dates.format_date(date=date, format='MMMM y', locale=get_lang(self.env).code)
-
- elif interval == 'dayname':
- # Localized day name
- result = babel.dates.format_date(date=date, format='EEEE', locale=get_lang(self.env).code)
-
- elif interval == 'time':
- # Localized time
- # FIXME: formats are specifically encoded to bytes, maybe use babel?
- dummy, format_time = self._get_date_formats()
- result = tools.ustr(date.strftime(format_time + " %Z"))
-
- return result
-
- def get_display_time_tz(self, tz=False):
- """ get the display_time of the meeting, forcing the timezone. This method is called from email template, to not use sudo(). """
- self.ensure_one()
- if tz:
- self = self.with_context(tz=tz)
- return self._get_display_time(self.start, self.stop, self.duration, self.allday)
-
- def action_open_calendar_event(self):
- if self.res_model and self.res_id:
- return self.env[self.res_model].browse(self.res_id).get_formview_action()
- return False
-
- def action_sendmail(self):
- email = self.env.user.email
- if email:
- for meeting in self:
- meeting.attendee_ids._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
- return True
-
- def action_mass_mailing(self):
- partners_ids = self.mapped('partner_ids')
- if not partners_ids:
- raise UserError(_("There are no attendees on these events"))
- compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
- default_partners = partners_ids and partners_ids.ids
- compose_ctx = dict(
- default_use_template=False,
- default_composition_mode='mass_mail',
- default_partner_ids=default_partners,
- default_subject=_("Event update")
- )
- return {
- 'name': _('Contact Attendees'),
- 'type': 'ir.actions.act_window',
- 'view_mode': 'form',
- 'res_model': 'mail.compose.message',
- 'views': [(compose_form.id, 'form')],
- 'view_id': compose_form.id,
- 'target': 'new',
- 'context': compose_ctx,
- }
-
- def _apply_recurrence_values(self, values, future=True):
- """Apply the new recurrence rules in `values`. Create a recurrence if it does not exist
- and create all missing events according to the rrule.
- If the changes are applied to future
- events only, a new recurrence is created with the updated rrule.
-
- :param values: new recurrence values to apply
- :param future: rrule values are applied to future events only if True.
- Rrule changes are applied to all events in the recurrence otherwise.
- (ignored if no recurrence exists yet).
- :return: events detached from the recurrence
- """
- if not values:
- return self.browse()
- recurrence_vals = []
- to_update = self.env['calendar.recurrence']
- for event in self:
- if not event.recurrence_id:
- recurrence_vals += [dict(values, base_event_id=event.id, calendar_event_ids=[(4, event.id)])]
- elif future:
- to_update |= event.recurrence_id._split_from(event, values)
- self.write({'recurrency': True, 'follow_recurrence': True})
- to_update |= self.env['calendar.recurrence'].create(recurrence_vals)
- return to_update._apply_recurrence()
-
- def _get_recurrence_params(self):
- if not self:
- return {}
- event_date = self._get_start_date()
- weekday_field_name = weekday_to_field(event_date.weekday())
- return {
- weekday_field_name: True,
- 'weekday': weekday_field_name.upper(),
- 'byday': str(get_weekday_occurence(event_date)),
- 'day': event_date.day,
- }
-
- def _get_start_date(self):
- """Return the event starting date in the event's timezone.
- If no starting time is assigned (yet), return today as default
- :return: date
- """
- if not self.start:
- return fields.Date.today()
- if self.recurrence_id.event_tz:
- tz = pytz.timezone(self.recurrence_id.event_tz)
- return pytz.utc.localize(self.start).astimezone(tz).date()
- return self.start.date()
-
- def _split_recurrence(self, time_values):
- """Apply time changes to events and update the recurrence accordingly.
-
- :return: detached events
- """
- if not time_values:
- return self.browse()
-
- previous_week_day_field = weekday_to_field(self._get_start_date().weekday())
- self.write(time_values)
- return self._apply_recurrence_values({
- previous_week_day_field: False,
- **self._get_recurrence_params(),
- }, future=True)
-
- def _break_recurrence(self, future=True):
- """Breaks the event's recurrence.
- Stop the recurrence at the current event if `future` is True, leaving past events in the recurrence.
- If `future` is False, all events in the recurrence are detached and the recurrence itself is unlinked.
- :return: detached events excluding the current events
- """
- recurrences_to_unlink = self.env['calendar.recurrence']
- detached_events = self.env['calendar.event']
- for event in self:
- recurrence = event.recurrence_id
- if future:
- detached_events |= recurrence._stop_at(event)
- else:
- detached_events |= recurrence.calendar_event_ids
- recurrence.calendar_event_ids.recurrence_id = False
- recurrences_to_unlink |= recurrence
- recurrences_to_unlink.with_context(archive_on_error=True).unlink()
- return detached_events - self
-
- def write(self, values):
- detached_events = self.env['calendar.event']
- recurrence_update_setting = values.pop('recurrence_update', None)
- update_recurrence = recurrence_update_setting in ('all_events', 'future_events') and len(self) == 1
- break_recurrence = values.get('recurrency') is False
-
- if 'partner_ids' in values:
- values['attendee_ids'] = self._attendees_values(values['partner_ids'])
-
- if (not recurrence_update_setting or recurrence_update_setting == 'self_only' and len(self) == 1) and 'follow_recurrence' not in values:
- if any({field: values.get(field) for field in self.env['calendar.event']._get_time_fields() if field in values}):
- values['follow_recurrence'] = False
-
- 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:
- detached_events |= self._break_recurrence(future=recurrence_update_setting == 'future_events')
- else:
- update_start = self.start if recurrence_update_setting == 'future_events' else None
- time_values = {field: values.pop(field) for field in self.env['calendar.event']._get_time_fields() if field in values}
- if not update_start and (time_values or recurrence_values):
- raise UserError(_("Updating All Events is not allowed when dates or time is modified. You can only update one particular event and following events."))
- detached_events |= self._split_recurrence(time_values)
- self.recurrence_id._write_events(values, dtstart=update_start)
- else:
- super().write(values)
- self._sync_activities(fields=values.keys())
-
- if recurrence_update_setting != 'self_only' and not break_recurrence:
- detached_events |= self._apply_recurrence_values(recurrence_values, future=recurrence_update_setting == 'future_events')
-
- (detached_events & self).active = False
- (detached_events - self).with_context(archive_on_error=True).unlink()
-
- self._setup_alarms()
-
- 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:
- start_date = fields.Datetime.to_datetime(values.get('start'))
- # Only notify on future events
- if start_date and start_date >= fields.Datetime.now():
- (current_attendees & previous_attendees)._send_mail_to_attendees('calendar.calendar_template_meeting_changedate', ignore_recurrence=not update_recurrence)
-
- return True
-
- def _get_trigger_alarm_types(self):
- return ['email']
-
- def _setup_alarms(self):
- """ Schedule cron triggers for future events """
- cron = self.env.ref('calendar.ir_cron_scheduler_alarm')
- alarm_manager = self.env['calendar.alarm_manager']
- alarm_types = self._get_trigger_alarm_types()
-
- for event in self:
- for alarm in (alarm for alarm in event.alarm_ids if alarm.alarm_type in alarm_types):
- at = event.start - timedelta(minutes=alarm.duration_minutes)
- if not cron.lastcall or at > cron.lastcall:
- # Don't trigger for past alarms, they would be skipped by design
- cron._trigger(at=at)
- if any(alarm.alarm_type == 'notification' for alarm in event.alarm_ids):
- alarm_manager._notify_next_alarm(event.partner_ids.ids)
+ # ------------------------------------------------------------
+ # CRUD
+ # ------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
@@ -783,7 +384,7 @@ class Meeting(models.Model):
detached_events.active = False
events.filtered(lambda event: event.start > fields.Datetime.now()).attendee_ids._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
- events._sync_activities(fields={f for vals in vals_list for f in vals.keys() })
+ events._sync_activities(fields={f for vals in vals_list for f in vals.keys()})
events._setup_alarms()
@@ -835,6 +436,59 @@ class Meeting(models.Model):
return public_events + my_private_events + obfuscated(others_private_events)
+ def write(self, values):
+ detached_events = self.env['calendar.event']
+ recurrence_update_setting = values.pop('recurrence_update', None)
+ update_recurrence = recurrence_update_setting in ('all_events', 'future_events') and len(self) == 1
+ break_recurrence = values.get('recurrency') is False
+
+ if 'partner_ids' in values:
+ values['attendee_ids'] = self._attendees_values(values['partner_ids'])
+
+ if (not recurrence_update_setting or recurrence_update_setting == 'self_only' and len(self) == 1) and 'follow_recurrence' not in values:
+ if any({field: values.get(field) for field in self.env['calendar.event']._get_time_fields() if field in values}):
+ values['follow_recurrence'] = False
+
+ 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:
+ detached_events |= self._break_recurrence(future=recurrence_update_setting == 'future_events')
+ else:
+ update_start = self.start if recurrence_update_setting == 'future_events' else None
+ time_values = {field: values.pop(field) for field in self.env['calendar.event']._get_time_fields() if field in values}
+ if not update_start and (time_values or recurrence_values):
+ raise UserError(_("Updating All Events is not allowed when dates or time is modified. You can only update one particular event and following events."))
+ detached_events |= self._split_recurrence(time_values)
+ self.recurrence_id._write_events(values, dtstart=update_start)
+ else:
+ super().write(values)
+ self._sync_activities(fields=values.keys())
+
+ if recurrence_update_setting != 'self_only' and not break_recurrence:
+ detached_events |= self._apply_recurrence_values(recurrence_values, future=recurrence_update_setting == 'future_events')
+
+ (detached_events & self).active = False
+ (detached_events - self).with_context(archive_on_error=True).unlink()
+
+ self._setup_alarms()
+
+ 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:
+ start_date = fields.Datetime.to_datetime(values.get('start'))
+ # Only notify on future events
+ if start_date and start_date >= fields.Datetime.now():
+ (current_attendees & previous_attendees).with_context(
+ calendar_template_ignore_recurrence=not update_recurrence
+ )._send_mail_to_attendees(
+ 'calendar.calendar_template_meeting_changedate'
+ )
+
+ return True
+
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
groupby = [groupby] if isinstance(groupby, str) else groupby
@@ -859,9 +513,80 @@ class Meeting(models.Model):
self.env['calendar.alarm_manager']._notify_next_alarm(partner_ids)
return result
- def _range(self):
- self.ensure_one()
- return (self.start, self.stop)
+ 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 = []
+
+ 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
+
+ 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_commands += [
+ [0, 0, dict(partner_id=partner_id)]
+ for partner_id in added_partner_ids
+ ]
+ return attendee_commands
+
+ # ------------------------------------------------------------
+ # ACTIONS
+ # ------------------------------------------------------------
+
+ def action_open_calendar_event(self):
+ if self.res_model and self.res_id:
+ return self.env[self.res_model].browse(self.res_id).get_formview_action()
+ return False
+
+ def action_sendmail(self):
+ email = self.env.user.email
+ if email:
+ for meeting in self:
+ meeting.attendee_ids._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
+ return True
+
+ def action_mass_mailing(self):
+ partners_ids = self.mapped('partner_ids')
+ if not partners_ids:
+ raise UserError(_("There are no attendees on these events"))
+ compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
+ default_partners = partners_ids and partners_ids.ids
+ compose_ctx = dict(
+ default_use_template=False,
+ default_composition_mode='mass_mail',
+ default_partner_ids=default_partners,
+ default_subject=_("Event update")
+ )
+ return {
+ 'name': _('Contact Attendees'),
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'mail.compose.message',
+ 'views': [(compose_form.id, 'form')],
+ 'view_id': compose_form.id,
+ 'target': 'new',
+ 'context': compose_ctx,
+ }
+
+ # ------------------------------------------------------------
+ # MAILING
+ # ------------------------------------------------------------
def _sync_activities(self, fields):
# update activities
@@ -879,7 +604,7 @@ class Meeting(models.Model):
deadline = event.start
user_tz = self.env.context.get('tz')
if user_tz and not event.allday:
- deadline = pytz.UTC.localize(deadline)
+ deadline = pytz.utc.localize(deadline)
deadline = deadline.astimezone(pytz.timezone(user_tz))
activity_values['date_deadline'] = deadline.date()
if 'user_id' in fields:
@@ -887,6 +612,107 @@ class Meeting(models.Model):
if activity_values.keys():
event.activity_ids.write(activity_values)
+ # ------------------------------------------------------------
+ # ALARMS
+ # ------------------------------------------------------------
+
+ def _get_trigger_alarm_types(self):
+ return ['email']
+
+ def _setup_alarms(self):
+ """ Schedule cron triggers for future events """
+ cron = self.env.ref('calendar.ir_cron_scheduler_alarm')
+ alarm_manager = self.env['calendar.alarm_manager']
+ alarm_types = self._get_trigger_alarm_types()
+
+ for event in self:
+ for alarm in (alarm for alarm in event.alarm_ids if alarm.alarm_type in alarm_types):
+ at = event.start - timedelta(minutes=alarm.duration_minutes)
+ if not cron.lastcall or at > cron.lastcall:
+ # Don't trigger for past alarms, they would be skipped by design
+ cron._trigger(at=at)
+ if any(alarm.alarm_type == 'notification' for alarm in event.alarm_ids):
+ alarm_manager._notify_next_alarm(event.partner_ids.ids)
+
+ # ------------------------------------------------------------
+ # RECURRENCY
+ # ------------------------------------------------------------
+
+ def _apply_recurrence_values(self, values, future=True):
+ """Apply the new recurrence rules in `values`. Create a recurrence if it does not exist
+ and create all missing events according to the rrule.
+ If the changes are applied to future
+ events only, a new recurrence is created with the updated rrule.
+
+ :param values: new recurrence values to apply
+ :param future: rrule values are applied to future events only if True.
+ Rrule changes are applied to all events in the recurrence otherwise.
+ (ignored if no recurrence exists yet).
+ :return: events detached from the recurrence
+ """
+ if not values:
+ return self.browse()
+ recurrence_vals = []
+ to_update = self.env['calendar.recurrence']
+ for event in self:
+ if not event.recurrence_id:
+ recurrence_vals += [dict(values, base_event_id=event.id, calendar_event_ids=[(4, event.id)])]
+ elif future:
+ to_update |= event.recurrence_id._split_from(event, values)
+ self.write({'recurrency': True, 'follow_recurrence': True})
+ to_update |= self.env['calendar.recurrence'].create(recurrence_vals)
+ return to_update._apply_recurrence()
+
+ def _get_recurrence_params(self):
+ if not self:
+ return {}
+ event_date = self._get_start_date()
+ weekday_field_name = weekday_to_field(event_date.weekday())
+ return {
+ weekday_field_name: True,
+ 'weekday': weekday_field_name.upper(),
+ 'byday': str(get_weekday_occurence(event_date)),
+ 'day': event_date.day,
+ }
+
+ def _split_recurrence(self, time_values):
+ """Apply time changes to events and update the recurrence accordingly.
+
+ :return: detached events
+ """
+ if not time_values:
+ return self.browse()
+
+ previous_week_day_field = weekday_to_field(self._get_start_date().weekday())
+ self.write(time_values)
+ return self._apply_recurrence_values({
+ previous_week_day_field: False,
+ **self._get_recurrence_params(),
+ }, future=True)
+
+ def _break_recurrence(self, future=True):
+ """Breaks the event's recurrence.
+ Stop the recurrence at the current event if `future` is True, leaving past events in the recurrence.
+ If `future` is False, all events in the recurrence are detached and the recurrence itself is unlinked.
+ :return: detached events excluding the current events
+ """
+ recurrences_to_unlink = self.env['calendar.recurrence']
+ detached_events = self.env['calendar.event']
+ for event in self:
+ recurrence = event.recurrence_id
+ if future:
+ detached_events |= recurrence._stop_at(event)
+ else:
+ detached_events |= recurrence.calendar_event_ids
+ recurrence.calendar_event_ids.recurrence_id = False
+ recurrences_to_unlink |= recurrence
+ recurrences_to_unlink.with_context(archive_on_error=True).unlink()
+ return detached_events - self
+
+ # ------------------------------------------------------------
+ # MANAGEMENT
+ # ------------------------------------------------------------
+
def change_attendee_status(self, status):
attendee = self.attendee_ids.filtered(lambda x: x.partner_id == self.env.user.partner_id)
if status == 'accepted':
@@ -894,3 +720,262 @@ class Meeting(models.Model):
if status == 'declined':
return attendee.do_decline()
return attendee.do_tentative()
+
+ def find_partner_customer(self):
+ self.ensure_one()
+ return next(
+ (attendee.partner_id for attendee in self.attendee_ids.sorted('create_date')
+ if attendee.partner_id != self.user_id.partner_id),
+ self.env['calendar.attendee']
+ )
+
+ # ------------------------------------------------------------
+ # TOOLS
+ # ------------------------------------------------------------
+
+ def _get_start_date(self):
+ """Return the event starting date in the event's timezone.
+ If no starting time is assigned (yet), return today as default
+ :return: date
+ """
+ if not self.start:
+ return fields.Date.today()
+ if self.recurrence_id.event_tz:
+ tz = pytz.timezone(self.recurrence_id.event_tz)
+ return pytz.utc.localize(self.start).astimezone(tz).date()
+ return self.start.date()
+
+ def _range(self):
+ self.ensure_one()
+ return (self.start, self.stop)
+
+ def get_interval(self, interval, tz=None):
+ """ Format and localize some dates to be used in email templates
+ :param string interval: Among 'day', 'month', 'dayname' and 'time' indicating the desired formatting
+ :param string tz: Timezone indicator (optional)
+ :return unicode: Formatted date or time (as unicode string, to prevent jinja2 crash)
+ """
+ self.ensure_one()
+ date = fields.Datetime.from_string(self.start)
+
+ if tz:
+ timezone = pytz.timezone(tz or 'UTC')
+ date = date.replace(tzinfo=pytz.timezone('UTC')).astimezone(timezone)
+
+ if interval == 'day':
+ # Day number (1-31)
+ result = str(date.day)
+
+ elif interval == 'month':
+ # Localized month name and year
+ result = babel.dates.format_date(date=date, format='MMMM y', locale=get_lang(self.env).code)
+
+ elif interval == 'dayname':
+ # Localized day name
+ result = babel.dates.format_date(date=date, format='EEEE', locale=get_lang(self.env).code)
+
+ elif interval == 'time':
+ # Localized time
+ # FIXME: formats are specifically encoded to bytes, maybe use babel?
+ dummy, format_time = self._get_date_formats()
+ result = tools.ustr(date.strftime(format_time + " %Z"))
+
+ return result
+
+ def get_display_time_tz(self, tz=False):
+ """ get the display_time of the meeting, forcing the timezone. This method is called from email template, to not use sudo(). """
+ self.ensure_one()
+ if tz:
+ self = self.with_context(tz=tz)
+ return self._get_display_time(self.start, self.stop, self.duration, self.allday)
+
+ def _get_ics_file(self):
+ """ Returns iCalendar file for the event invitation.
+ :returns a dict of .ics file content for each meeting
+ """
+ result = {}
+
+ def ics_datetime(idate, allday=False):
+ if idate:
+ if allday:
+ return idate
+ return idate.replace(tzinfo=pytz.timezone('UTC'))
+ return False
+
+ if not vobject:
+ return result
+
+ for meeting in self:
+ cal = vobject.iCalendar()
+ event = cal.add('vevent')
+
+ if not meeting.start or not meeting.stop:
+ raise UserError(_("First you have to specify the date of the invitation."))
+ event.add('created').value = ics_datetime(fields.Datetime.now())
+ event.add('dtstart').value = ics_datetime(meeting.start, meeting.allday)
+ event.add('dtend').value = ics_datetime(meeting.stop, meeting.allday)
+ event.add('summary').value = meeting.name
+ if meeting.description:
+ event.add('description').value = meeting.description
+ if meeting.location:
+ event.add('location').value = meeting.location
+ if meeting.rrule:
+ event.add('rrule').value = meeting.rrule
+
+ if meeting.alarm_ids:
+ for alarm in meeting.alarm_ids:
+ valarm = event.add('valarm')
+ interval = alarm.interval
+ duration = alarm.duration
+ trigger = valarm.add('TRIGGER')
+ trigger.params['related'] = ["START"]
+ if interval == 'days':
+ delta = timedelta(days=duration)
+ elif interval == 'hours':
+ delta = timedelta(hours=duration)
+ elif interval == 'minutes':
+ delta = timedelta(minutes=duration)
+ trigger.value = delta
+ valarm.add('DESCRIPTION').value = alarm.name or u'Odoo'
+ for attendee in meeting.attendee_ids:
+ attendee_add = event.add('attendee')
+ attendee_add.value = u'MAILTO:' + (attendee.email or u'')
+ result[meeting.id] = cal.serialize().encode('utf-8')
+
+ return result
+
+ @api.model
+ def _get_display_time(self, start, stop, zduration, zallday):
+ """ Return date and time (from to from) based on duration with timezone in string. Eg :
+ 1) if user add duration for 2 hours, return : August-23-2013 at (04-30 To 06-30) (Europe/Brussels)
+ 2) if event all day ,return : AllDay, July-31-2013
+ """
+ timezone = self._context.get('tz') or self.env.user.partner_id.tz or 'UTC'
+
+ # get date/time format according to context
+ format_date, format_time = self._get_date_formats()
+
+ # convert date and time into user timezone
+ self_tz = self.with_context(tz=timezone)
+ date = fields.Datetime.context_timestamp(self_tz, fields.Datetime.from_string(start))
+ date_deadline = fields.Datetime.context_timestamp(self_tz, fields.Datetime.from_string(stop))
+
+ # convert into string the date and time, using user formats
+ to_text = pycompat.to_text
+ date_str = to_text(date.strftime(format_date))
+ time_str = to_text(date.strftime(format_time))
+
+ if zallday:
+ display_time = _("All Day, %(day)s", day=date_str)
+ elif zduration < 24:
+ duration = date + timedelta(minutes=round(zduration*60))
+ duration_time = to_text(duration.strftime(format_time))
+ display_time = _(
+ u"%(day)s at (%(start)s To %(end)s) (%(timezone)s)",
+ day=date_str,
+ start=time_str,
+ end=duration_time,
+ timezone=timezone,
+ )
+ else:
+ dd_date = to_text(date_deadline.strftime(format_date))
+ dd_time = to_text(date_deadline.strftime(format_time))
+ display_time = _(
+ u"%(date_start)s at %(time_start)s To\n %(date_end)s at %(time_end)s (%(timezone)s)",
+ date_start=date_str,
+ time_start=time_str,
+ date_end=dd_date,
+ time_end=dd_time,
+ timezone=timezone,
+ )
+ return display_time
+
+ def _get_duration(self, start, stop):
+ """ Get the duration value between the 2 given dates. """
+ if not start or not stop:
+ return 0
+ duration = (stop - start).total_seconds() / 3600
+ return round(duration, 2)
+
+ @api.model
+ def _get_date_formats(self):
+ """ get current date and time format, according to the context lang
+ :return: a tuple with (format date, format time)
+ """
+ lang = get_lang(self.env)
+ return (lang.date_format, lang.time_format)
+
+ @api.model
+ def _get_recurrent_fields(self):
+ return {'byday', 'until', 'rrule_type', 'month_by', 'event_tz', 'rrule',
+ 'interval', 'count', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa',
+ 'su', 'day', 'weekday'}
+
+ @api.model
+ def _get_time_fields(self):
+ return {'start', 'stop', 'start_date', 'stop_date'}
+
+ @api.model
+ def _get_custom_fields(self):
+ all_fields = self.fields_get(attributes=['manual'])
+ return {fname for fname in all_fields if all_fields[fname]['manual']}
+
+ @api.model
+ def _get_public_fields(self):
+ return self._get_recurrent_fields() | self._get_time_fields() | self._get_custom_fields() | {
+ 'id', 'active', 'allday',
+ 'duration', 'user_id', 'interval',
+ 'count', 'rrule', 'recurrence_id', 'show_as', 'privacy'}
+
+ def description_to_html_lines(self):
+ """ Description could contain some structure content, depending on
+ its use. Notably appointment could add some structured data in it
+ in addition to free text. Purpose of this method is to generate a
+ simple html.
+
+ Input (self.description) example:
+Some free text salespeople added
+as multi line
+ * Mobile: +320475000000
+ * Email: my.email@test.example.com
+ * SingleLine: answer
+ * MultiLine:
+Answer1
+Answer2
+Answer3
+ * Dropdown: answer
+ * Radio: answer
+ * Checkboxes: answer1, answer2
+Some free text salespeople added
+as multi line
+
+ Output: a list of items[
+'Some free text salespeople added as multi line',
+'Mobile: +320475000000',
+'Email: my.email@test.example.com',
+'SingleLine: answer',
+'MultiLine: Answer1 Answer2 Answer3',
+'Dropdown: answer',
+'Radio: answer',
+'Checkboxes: answer1, answer2',
+'Some free text salespeople added as multi line']
+
+ Each item of returned list is escaped so that only our intended
+ are html tags. It should therefore be safe.
+ """
+
+ final_lines = []
+ parsed_lines = []
+ for line in self.description.split('\n'):
+ if not line.strip():
+ continue
+ # new line
+ if line.startswith(' *'):
+ if parsed_lines:
+ final_lines.append(' '.join(html_escape(line) for line in parsed_lines))
+ parsed_lines = [line.lstrip(' *')]
+ else:
+ parsed_lines.append(line)
+ if parsed_lines:
+ final_lines.append(' '.join(html_escape(line) for line in parsed_lines))
+ return final_lines
diff --git a/addons/google_calendar/models/calendar_attendee.py b/addons/google_calendar/models/calendar_attendee.py
index d62702f8972..52327cfc2aa 100644
--- a/addons/google_calendar/models/calendar_attendee.py
+++ b/addons/google_calendar/models/calendar_attendee.py
@@ -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)
diff --git a/addons/microsoft_calendar/models/calendar_attendee.py b/addons/microsoft_calendar/models/calendar_attendee.py
index 9a462c38f70..5da93635099 100644
--- a/addons/microsoft_calendar/models/calendar_attendee.py
+++ b/addons/microsoft_calendar/models/calendar_attendee.py
@@ -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)
|
|