diff options
| author | Lucas Lefèvre <lul@odoo.com> | 2019-11-26 13:24:14 +0100 |
|---|---|---|
| committer | Lucas Lefèvre <lul@odoo.com> | 2020-04-09 10:53:46 +0200 |
| commit | 779912007441d005641daadbd8336029950cc7d0 (patch) | |
| tree | 6a7bb75429d1184d680974ee926485391c23bb2e /addons | |
| parent | 60b5220c7230464157f21a8a3fc12c909ca7eb79 (diff) | |
[MOV] calendar: Split models in their own file
Apply Odoo guidelines[1].
Each model should be in a different file.
Task 2126717
PR #42031
PR Enterprise odoo/enterprise#8006
[1] https://www.odoo.com/documentation/13.0/reference/guidelines.html#file-naming
Diffstat (limited to 'addons')
| -rw-r--r-- | addons/calendar/models/__init__.py | 8 | ||||
| -rw-r--r-- | addons/calendar/models/calendar_alarm.py | 58 | ||||
| -rw-r--r-- | addons/calendar/models/calendar_alarm_manager.py | 247 | ||||
| -rw-r--r-- | addons/calendar/models/calendar_attendee.py | 139 | ||||
| -rw-r--r-- | addons/calendar/models/calendar_contact.py | 21 | ||||
| -rw-r--r-- | addons/calendar/models/calendar_event.py (renamed from addons/calendar/models/calendar.py) | 458 | ||||
| -rw-r--r-- | addons/calendar/models/calendar_event_type.py | 16 | ||||
| -rw-r--r-- | addons/calendar/models/ir_attachment.py | 2 | ||||
| -rw-r--r-- | addons/calendar/models/mail_message.py | 2 | ||||
| -rw-r--r-- | addons/calendar/models/res_partner.py | 2 | ||||
| -rw-r--r-- | addons/calendar/tests/test_calendar_recurrent_event_case2.py | 2 | ||||
| -rw-r--r-- | addons/calendar/wizard/mail_invite.py | 2 |
12 files changed, 496 insertions, 461 deletions
diff --git a/addons/calendar/models/__init__.py b/addons/calendar/models/__init__.py index e83190ac8a0..7dc8d472244 100644 --- a/addons/calendar/models/__init__.py +++ b/addons/calendar/models/__init__.py @@ -5,6 +5,12 @@ from . import ir_attachment from . import ir_http from . import res_partner from . import mail_message -from . import calendar +from . import calendar_event +from . import calendar_alarm +from . import calendar_alarm_manager +from . import calendar_attendee +from . import calendar_contact +from . import calendar_event_type +from . import calendar_event from . import mail_activity from . import res_users diff --git a/addons/calendar/models/calendar_alarm.py b/addons/calendar/models/calendar_alarm.py new file mode 100644 index 00000000000..4bcaa6e28d6 --- /dev/null +++ b/addons/calendar/models/calendar_alarm.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class Alarm(models.Model): + _name = 'calendar.alarm' + _description = 'Event Alarm' + + @api.depends('interval', 'duration') + def _compute_duration_minutes(self): + for alarm in self: + if alarm.interval == "minutes": + alarm.duration_minutes = alarm.duration + elif alarm.interval == "hours": + alarm.duration_minutes = alarm.duration * 60 + elif alarm.interval == "days": + alarm.duration_minutes = alarm.duration * 60 * 24 + else: + alarm.duration_minutes = 0 + + _interval_selection = {'minutes': 'Minutes', 'hours': 'Hours', 'days': 'Days'} + + name = fields.Char('Name', translate=True, required=True) + alarm_type = fields.Selection([('notification', 'Notification'), ('email', 'Email')], string='Type', required=True, default='email') + duration = fields.Integer('Remind Before', required=True, default=1) + interval = fields.Selection(list(_interval_selection.items()), 'Unit', required=True, default='hours') + duration_minutes = fields.Integer('Duration in minutes', compute='_compute_duration_minutes', store=True, help="Duration in minutes") + + @api.onchange('duration', 'interval', 'alarm_type') + def _onchange_duration_interval(self): + display_interval = self._interval_selection.get(self.interval, '') + display_alarm_type = {key: value for key, value in self._fields['alarm_type']._description_selection(self.env)}[self.alarm_type] + self.name = "%s - %s %s" % (display_alarm_type, self.duration, display_interval) + + def _update_cron(self): + try: + cron = self.env['ir.model.data'].sudo().get_object('calendar', 'ir_cron_scheduler_alarm') + except ValueError: + return False + return cron.toggle(model=self._name, domain=[('alarm_type', '=', 'email')]) + + @api.model + def create(self, values): + result = super(Alarm, self).create(values) + self._update_cron() + return result + + def write(self, values): + result = super(Alarm, self).write(values) + self._update_cron() + return result + + def unlink(self): + result = super(Alarm, self).unlink() + self._update_cron() + return result diff --git a/addons/calendar/models/calendar_alarm_manager.py b/addons/calendar/models/calendar_alarm_manager.py new file mode 100644 index 00000000000..e6809a6b74a --- /dev/null +++ b/addons/calendar/models/calendar_alarm_manager.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import datetime +from datetime import timedelta + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +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): + result = {} + delta_request = """ + SELECT + rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta + FROM + calendar_alarm_calendar_event_rel AS rel + LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id + WHERE alarm.alarm_type = %s + GROUP BY rel.calendar_event_id + """ + base_request = """ + SELECT + cal.id, + cal.start - interval '1' minute * calcul_delta.max_delta AS first_alarm, + CASE + WHEN cal.recurrency THEN cal.final_date - interval '1' minute * calcul_delta.min_delta + ELSE cal.stop - interval '1' minute * calcul_delta.min_delta + END as last_alarm, + cal.start as first_event_date, + CASE + WHEN cal.recurrency THEN cal.final_date + ELSE cal.stop + END as last_event_date, + calcul_delta.min_delta, + calcul_delta.max_delta, + cal.rrule AS rule + FROM + calendar_event AS cal + RIGHT JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id + """ + + 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 + """ + + # Add filter on alarm type + tuple_params = (alarm_type,) + + # Add filter on partner_id + if partner_id: + base_request += filter_user + tuple_params += (partner_id, ) + + # Upper bound on first_alarm of requested events + first_alarm_max_value = "" + if seconds is None: + # first alarm in the future + 3 minutes if there is one, now otherwise + first_alarm_max_value = """ + COALESCE((SELECT MIN(cal.start - interval '1' minute * calcul_delta.max_delta) + FROM calendar_event cal + RIGHT JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id + WHERE cal.start - interval '1' minute * calcul_delta.max_delta > now() at time zone 'utc' + ) + interval '3' minute, now() at time zone 'utc')""" + else: + # now + given seconds + first_alarm_max_value = "(now() at time zone 'utc' + interval '%s' second )" + tuple_params += (seconds,) + + self._cr.execute(""" + WITH calcul_delta AS (%s) + SELECT * + FROM ( %s WHERE cal.active = True ) AS ALL_EVENTS + WHERE ALL_EVENTS.first_alarm < %s + AND ALL_EVENTS.last_event_date > (now() at time zone 'utc') + """ % (delta_request, base_request, first_alarm_max_value), tuple_params) + + for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in self._cr.fetchall(): + result[event_id] = { + 'event_id': event_id, + 'first_alarm': first_alarm, + 'last_alarm': last_alarm, + 'first_meeting': first_meeting, + 'last_meeting': last_meeting, + 'min_duration': min_duration, + 'max_duration': max_duration, + 'rrule': rule + } + + # determine accessible events + events = self.env['calendar.event'].browse(result) + result = { + key: result[key] + for key in set(events._filter_access_rules('read').ids) + } + return result + + def do_check_alarm_for_one_date(self, one_date, event, event_maxdelta, in_the_next_X_seconds, alarm_type, after=False, missing=False): + """ Search for some alarms in the interval of time determined by some parameters (after, in_the_next_X_seconds, ...) + :param one_date: date of the event to check (not the same that in the event browse if recurrent) + :param event: Event browse record + :param event_maxdelta: biggest duration from alarms for this event + :param in_the_next_X_seconds: looking in the future (in seconds) + :param after: if not False: will return alert if after this date (date as string - todo: change in master) + :param missing: if not False: will return alert even if we are too late + :param notif: Looking for type notification + :param mail: looking for type email + """ + 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 + 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 \ + (not after or one_date - timedelta(minutes=alarm.duration_minutes) > fields.Datetime.from_string(after)): + alert = { + 'alarm_id': alarm.id, + 'event_id': event.id, + 'notify_at': one_date - timedelta(minutes=alarm.duration_minutes), + } + result.append(alert) + return result + + @api.model + def get_next_mail(self): + 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) + if not cron: + _logger.error("Cron for " + self._name + " can not be identified !") + return False + + interval_to_second = { + "weeks": 7 * 24 * 60 * 60, + "days": 24 * 60 * 60, + "hours": 60 * 60, + "minutes": 60, + "seconds": 1 + } + + if cron.interval_type not in interval_to_second: + _logger.error("Cron delay can not be computed !") + return False + + cron_interval = cron.interval_number * interval_to_second[cron.interval_type] + + all_meetings = self._get_next_potential_limit_alarm('email', seconds=cron_interval) + + for meeting in self.env['calendar.event'].browse(all_meetings): + max_delta = all_meetings[meeting.id]['max_duration'] + + if meeting.recurrency: + at_least_one = False + last_found = False + for one_date in meeting._get_recurrent_date_by_event(): + in_date_format = one_date.replace(tzinfo=None) + last_found = self.do_check_alarm_for_one_date(in_date_format, meeting, max_delta, 0, 'email', after=last_notif_mail, missing=True) + for alert in last_found: + self.do_mail_reminder(alert) + at_least_one = True # if it's the first alarm for this recurrent event + if at_least_one and not last_found: # if the precedent event had an alarm but not this one, we can stop the search for this event + break + else: + in_date_format = meeting.start + last_found = self.do_check_alarm_for_one_date(in_date_format, meeting, max_delta, 0, 'email', after=last_notif_mail, missing=True) + for alert in last_found: + self.do_mail_reminder(alert) + + @api.model + def get_next_notif(self): + partner = self.env.user.partner_id + all_notif = [] + + if not partner: + return [] + + all_meetings = self._get_next_potential_limit_alarm('notification', partner_id=partner.id) + 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'] + meeting = self.env['calendar.event'].browse(event_id) + if meeting.recurrency: + b_found = False + last_found = False + for one_date in meeting._get_recurrent_date_by_event(): + in_date_format = one_date.replace(tzinfo=None) + last_found = self.do_check_alarm_for_one_date(in_date_format, meeting, max_delta, time_limit, 'notification', after=partner.calendar_last_notif_ack) + if last_found: + for alert in last_found: + all_notif.append(self.do_notif_reminder(alert)) + if not b_found: # if it's the first alarm for this recurrent event + b_found = True + if b_found and not last_found: # if the precedent event had alarm but not this one, we can stop the search fot this event + break + else: + in_date_format = fields.Datetime.from_string(meeting.start) + last_found = self.do_check_alarm_for_one_date(in_date_format, meeting, max_delta, time_limit, 'notification', after=partner.calendar_last_notif_ack) + if last_found: + for alert in last_found: + all_notif.append(self.do_notif_reminder(alert)) + return all_notif + + def do_mail_reminder(self, alert): + meeting = self.env['calendar.event'].browse(alert['event_id']) + alarm = self.env['calendar.alarm'].browse(alert['alarm_id']) + + 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) + return result + + def do_notif_reminder(self, alert): + alarm = self.env['calendar.alarm'].browse(alert['alarm_id']) + meeting = self.env['calendar.event'].browse(alert['event_id']) + + if alarm.alarm_type == 'notification': + message = meeting.display_time + + delta = alert['notify_at'] - datetime.datetime.now() + delta = delta.seconds + delta.days * 3600 * 24 + + return { + 'alarm_id': alarm.id, + 'event_id': meeting.id, + 'title': meeting.name, + 'message': message, + 'timer': delta, + 'notify_at': fields.Datetime.to_string(alert['notify_at']), + } + + def _notify_next_alarm(self, partner_ids): + """ Sends through the bus the next alarm of given partners """ + notifications = [] + users = self.env['res.users'].search([('partner_id', 'in', tuple(partner_ids))]) + for user in users: + notif = self.with_user(user).get_next_notif() + notifications.append([(self._cr.dbname, 'calendar.alarm', user.partner_id.id), notif]) + if len(notifications) > 0: + self.env['bus.bus'].sendmany(notifications) diff --git a/addons/calendar/models/calendar_attendee.py b/addons/calendar/models/calendar_attendee.py new file mode 100644 index 00000000000..765258274f0 --- /dev/null +++ b/addons/calendar/models/calendar_attendee.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import uuid +import base64 + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class Attendee(models.Model): + """ Calendar Attendee Information """ + + _name = 'calendar.attendee' + _rec_name = 'common_name' + _description = 'Calendar Attendee Information' + + def _default_access_token(self): + return uuid.uuid4().hex + + STATE_SELECTION = [ + ('needsAction', 'Needs Action'), + ('tentative', 'Uncertain'), + ('declined', 'Declined'), + ('accepted', 'Accepted'), + ] + + 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") + 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') + + @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 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) + + @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): + """ 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) + """ + 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) + + # 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() + + # prepare rendering context for mail template + colors = { + 'needsAction': 'grey', + 'accepted': 'green', + 'tentative': '#FFFF00', + 'declined': 'red' + } + rendering_context = dict(self._context) + rendering_context.update({ + 'color': colors, + '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: + # FIXME: is ics_file text or bytes? + event_id = force_event_id.id if force_event_id else 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'] = [ + (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 + + def do_tentative(self): + """ Makes event invitation as Tentative. """ + return self.write({'state': 'tentative'}) + + 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 + + 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 + diff --git a/addons/calendar/models/calendar_contact.py b/addons/calendar/models/calendar_contact.py new file mode 100644 index 00000000000..1356e96714f --- /dev/null +++ b/addons/calendar/models/calendar_contact.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class Contacts(models.Model): + _name = 'calendar.contacts' + _description = 'Calendar Contacts' + + user_id = fields.Many2one('res.users', 'Me', required=True, default=lambda self: self.env.user) + partner_id = fields.Many2one('res.partner', 'Employee', required=True) + active = fields.Boolean('Active', default=True) + + _sql_constraints = [ + ('user_id_partner_id_unique', 'UNIQUE(user_id, partner_id)', 'A user cannot have twice the same contact.') + ] + + @api.model + def unlink_from_partner_id(self, partner_id): + return self.search([('partner_id', '=', partner_id)]).unlink() diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar_event.py index 8c26ba752b4..91e5790fb9a 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar_event.py @@ -1,23 +1,20 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import base64 import babel.dates -import collections import datetime from datetime import timedelta, MAXYEAR from dateutil import rrule from dateutil.relativedelta import relativedelta import logging -from operator import itemgetter import pytz import re import time -import uuid from odoo import api, fields, models from odoo import tools from odoo.addons.base.models.res_partner import _tz_get +from odoo.addons.calendar.models.calendar_attendee import Attendee from odoo.osv import expression from odoo.tools.translate import _ from odoo.tools.misc import get_lang @@ -61,6 +58,7 @@ def get_real_ids(ids): def real_id2calendar_id(record_id, date): return '%s-%s' % (record_id, date.strftime(VIRTUALID_DATETIME_FORMAT)) + def any_id2key(record_id): """ Creates a (real_id: int, thing: str) pair which allows ordering mixed collections of real and virtual events. @@ -88,461 +86,11 @@ SORT_ALIASES = { 'start_date': 'sort_start', 'start_datetime': 'sort_start', } + def sort_remap(f): return SORT_ALIASES.get(f, f) -class Contacts(models.Model): - _name = 'calendar.contacts' - _description = 'Calendar Contacts' - - user_id = fields.Many2one('res.users', 'Me', required=True, default=lambda self: self.env.user) - partner_id = fields.Many2one('res.partner', 'Employee', required=True) - active = fields.Boolean('Active', default=True) - - _sql_constraints = [ - ('user_id_partner_id_unique', 'UNIQUE(user_id,partner_id)', 'An user cannot have twice the same contact.') - ] - - @api.model - def unlink_from_partner_id(self, partner_id): - return self.search([('partner_id', '=', partner_id)]).unlink() - - -class Attendee(models.Model): - """ Calendar Attendee Information """ - - _name = 'calendar.attendee' - _rec_name = 'common_name' - _description = 'Calendar Attendee Information' - - def _default_access_token(self): - return uuid.uuid4().hex - - STATE_SELECTION = [ - ('needsAction', 'Needs Action'), - ('tentative', 'Uncertain'), - ('declined', 'Declined'), - ('accepted', 'Accepted'), - ] - - 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") - 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') - - @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 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) - - @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): - """ 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) - """ - 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) - - # 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() - - # prepare rendering context for mail template - colors = { - 'needsAction': 'grey', - 'accepted': 'green', - 'tentative': '#FFFF00', - 'declined': 'red' - } - rendering_context = dict(self._context) - rendering_context.update({ - 'color': colors, - '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: - # FIXME: is ics_file text or bytes? - event_id = force_event_id.id if force_event_id else 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'] = [ - (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'].sudo().browse(mail_ids).send() - - return res - - def do_tentative(self): - """ Makes event invitation as Tentative. """ - return self.write({'state': 'tentative'}) - - 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 - - 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 - - -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): - result = {} - delta_request = """ - SELECT - rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta - FROM - calendar_alarm_calendar_event_rel AS rel - LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id - WHERE alarm.alarm_type = %s - GROUP BY rel.calendar_event_id - """ - base_request = """ - SELECT - cal.id, - cal.start - interval '1' minute * calcul_delta.max_delta AS first_alarm, - CASE - WHEN cal.recurrency THEN cal.final_date - interval '1' minute * calcul_delta.min_delta - ELSE cal.stop - interval '1' minute * calcul_delta.min_delta - END as last_alarm, - cal.start as first_event_date, - CASE - WHEN cal.recurrency THEN cal.final_date - ELSE cal.stop - END as last_event_date, - calcul_delta.min_delta, - calcul_delta.max_delta, - cal.rrule AS rule - FROM - calendar_event AS cal - RIGHT JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id - """ - - 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 - """ - - # Add filter on alarm type - tuple_params = (alarm_type,) - - # Add filter on partner_id - if partner_id: - base_request += filter_user - tuple_params += (partner_id, ) - - # Upper bound on first_alarm of requested events - first_alarm_max_value = "" - if seconds is None: - # first alarm in the future + 3 minutes if there is one, now otherwise - first_alarm_max_value = """ - COALESCE((SELECT MIN(cal.start - interval '1' minute * calcul_delta.max_delta) - FROM calendar_event cal - RIGHT JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id - WHERE cal.start - interval '1' minute * calcul_delta.max_delta > now() at time zone 'utc' - ) + interval '3' minute, now() at time zone 'utc')""" - else: - # now + given seconds - first_alarm_max_value = "(now() at time zone 'utc' + interval '%s' second )" - tuple_params += (seconds,) - - self._cr.execute(""" - WITH calcul_delta AS (%s) - SELECT * - FROM ( %s WHERE cal.active = True ) AS ALL_EVENTS - WHERE ALL_EVENTS.first_alarm < %s - AND ALL_EVENTS.last_event_date > (now() at time zone 'utc') - """ % (delta_request, base_request, first_alarm_max_value), tuple_params) - - for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in self._cr.fetchall(): - result[event_id] = { - 'event_id': event_id, - 'first_alarm': first_alarm, - 'last_alarm': last_alarm, - 'first_meeting': first_meeting, - 'last_meeting': last_meeting, - 'min_duration': min_duration, - 'max_duration': max_duration, - 'rrule': rule - } - - # determine accessible events - events = self.env['calendar.event'].browse(result) - result = { - key: result[key] - for key in set(events._filter_access_rules('read').ids) - } - return result - - def do_check_alarm_for_one_date(self, one_date, event, event_maxdelta, in_the_next_X_seconds, alarm_type, after=False, missing=False): - """ Search for some alarms in the interval of time determined by some parameters (after, in_the_next_X_seconds, ...) - :param one_date: date of the event to check (not the same that in the event browse if recurrent) - :param event: Event browse record - :param event_maxdelta: biggest duration from alarms for this event - :param in_the_next_X_seconds: looking in the future (in seconds) - :param after: if not False: will return alert if after this date (date as string - todo: change in master) - :param missing: if not False: will return alert even if we are too late - :param notif: Looking for type notification - :param mail: looking for type email - """ - 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 - 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 \ - (not after or one_date - timedelta(minutes=alarm.duration_minutes) > fields.Datetime.from_string(after)): - alert = { - 'alarm_id': alarm.id, - 'event_id': event.id, - 'notify_at': one_date - timedelta(minutes=alarm.duration_minutes), - } - result.append(alert) - return result - - @api.model - def get_next_mail(self): - 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) - if not cron: - _logger.error("Cron for " + self._name + " can not be identified !") - return False - - interval_to_second = { - "weeks": 7 * 24 * 60 * 60, - "days": 24 * 60 * 60, - "hours": 60 * 60, - "minutes": 60, - "seconds": 1 - } - - if cron.interval_type not in interval_to_second: - _logger.error("Cron delay can not be computed !") - return False - - cron_interval = cron.interval_number * interval_to_second[cron.interval_type] - - all_meetings = self._get_next_potential_limit_alarm('email', seconds=cron_interval) - - for meeting in self.env['calendar.event'].browse(all_meetings): - max_delta = all_meetings[meeting.id]['max_duration'] - - if meeting.recurrency: - at_least_one = False - last_found = False - for one_date in meeting._get_recurrent_date_by_event(): - in_date_format = one_date.replace(tzinfo=None) - last_found = self.do_check_alarm_for_one_date(in_date_format, meeting, max_delta, 0, 'email', after=last_notif_mail, missing=True) - for alert in last_found: - self.do_mail_reminder(alert) - at_least_one = True # if it's the first alarm for this recurrent event - if at_least_one and not last_found: # if the precedent event had an alarm but not this one, we can stop the search for this event - break - else: - in_date_format = meeting.start - last_found = self.do_check_alarm_for_one_date(in_date_format, meeting, max_delta, 0, 'email', after=last_notif_mail, missing=True) - for alert in last_found: - self.do_mail_reminder(alert) - - @api.model - def get_next_notif(self): - partner = self.env.user.partner_id - all_notif = [] - - if not partner: - return [] - - all_meetings = self._get_next_potential_limit_alarm('notification', partner_id=partner.id) - 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'] - meeting = self.env['calendar.event'].browse(event_id) - if meeting.recurrency: - b_found = False - last_found = False - for one_date in meeting._get_recurrent_date_by_event(): - in_date_format = one_date.replace(tzinfo=None) - last_found = self.do_check_alarm_for_one_date(in_date_format, meeting, max_delta, time_limit, 'notification', after=partner.calendar_last_notif_ack) - if last_found: - for alert in last_found: - all_notif.append(self.do_notif_reminder(alert)) - if not b_found: # if it's the first alarm for this recurrent event - b_found = True - if b_found and not last_found: # if the precedent event had alarm but not this one, we can stop the search fot this event - break - else: - in_date_format = fields.Datetime.from_string(meeting.start) - last_found = self.do_check_alarm_for_one_date(in_date_format, meeting, max_delta, time_limit, 'notification', after=partner.calendar_last_notif_ack) - if last_found: - for alert in last_found: - all_notif.append(self.do_notif_reminder(alert)) - return all_notif - - def do_mail_reminder(self, alert): - meeting = self.env['calendar.event'].browse(alert['event_id']) - alarm = self.env['calendar.alarm'].browse(alert['alarm_id']) - - 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) - return result - - def do_notif_reminder(self, alert): - alarm = self.env['calendar.alarm'].browse(alert['alarm_id']) - meeting = self.env['calendar.event'].browse(alert['event_id']) - - if alarm.alarm_type == 'notification': - message = meeting.display_time - - delta = alert['notify_at'] - datetime.datetime.now() - delta = delta.seconds + delta.days * 3600 * 24 - - return { - 'alarm_id': alarm.id, - 'event_id': meeting.id, - 'title': meeting.name, - 'message': message, - 'timer': delta, - 'notify_at': fields.Datetime.to_string(alert['notify_at']), - } - - def _notify_next_alarm(self, partner_ids): - """ Sends through the bus the next alarm of given partners """ - notifications = [] - users = self.env['res.users'].search([('partner_id', 'in', tuple(partner_ids))]) - for user in users: - notif = self.with_user(user).get_next_notif() - notifications.append([(self._cr.dbname, 'calendar.alarm', user.partner_id.id), notif]) - if len(notifications) > 0: - self.env['bus.bus'].sendmany(notifications) - - -class Alarm(models.Model): - _name = 'calendar.alarm' - _description = 'Event Alarm' - - @api.depends('interval', 'duration') - def _compute_duration_minutes(self): - for alarm in self: - if alarm.interval == "minutes": - alarm.duration_minutes = alarm.duration - elif alarm.interval == "hours": - alarm.duration_minutes = alarm.duration * 60 - elif alarm.interval == "days": - alarm.duration_minutes = alarm.duration * 60 * 24 - else: - alarm.duration_minutes = 0 - - _interval_selection = {'minutes': 'Minutes', 'hours': 'Hours', 'days': 'Days'} - - name = fields.Char('Name', translate=True, required=True) - alarm_type = fields.Selection([('notification', 'Notification'), ('email', 'Email')], string='Type', required=True, default='email') - duration = fields.Integer('Remind Before', required=True, default=1) - interval = fields.Selection(list(_interval_selection.items()), 'Unit', required=True, default='hours') - duration_minutes = fields.Integer('Duration in minutes', compute='_compute_duration_minutes', store=True, help="Duration in minutes") - - @api.onchange('duration', 'interval', 'alarm_type') - def _onchange_duration_interval(self): - display_interval = self._interval_selection.get(self.interval, '') - display_alarm_type = {key: value for key, value in self._fields['alarm_type']._description_selection(self.env)}[self.alarm_type] - self.name = "%s - %s %s" % (display_alarm_type, self.duration, display_interval) - - def _update_cron(self): - try: - cron = self.env['ir.model.data'].sudo().get_object('calendar', 'ir_cron_scheduler_alarm') - except ValueError: - return False - return cron.toggle(model=self._name, domain=[('alarm_type', '=', 'email')]) - - @api.model - def create(self, values): - result = super(Alarm, self).create(values) - self._update_cron() - return result - - def write(self, values): - result = super(Alarm, self).write(values) - self._update_cron() - return result - - def unlink(self): - result = super(Alarm, self).unlink() - self._update_cron() - return result - - -class MeetingType(models.Model): - - _name = 'calendar.event.type' - _description = 'Event Meeting Type' - - name = fields.Char('Name', required=True) - - _sql_constraints = [ - ('name_uniq', 'unique (name)', "Tag name already exists !"), - ] - - class Meeting(models.Model): """ Model for Calendar Event diff --git a/addons/calendar/models/calendar_event_type.py b/addons/calendar/models/calendar_event_type.py new file mode 100644 index 00000000000..14d92ebca38 --- /dev/null +++ b/addons/calendar/models/calendar_event_type.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class MeetingType(models.Model): + + _name = 'calendar.event.type' + _description = 'Event Meeting Type' + + name = fields.Char('Name', required=True) + + _sql_constraints = [ + ('name_uniq', 'unique (name)', "Tag name already exists !"), + ] diff --git a/addons/calendar/models/ir_attachment.py b/addons/calendar/models/ir_attachment.py index dd81fd35e91..b985c2d206d 100644 --- a/addons/calendar/models/ir_attachment.py +++ b/addons/calendar/models/ir_attachment.py @@ -3,7 +3,7 @@ from odoo import api, models -from odoo.addons.calendar.models.calendar import get_real_ids +from odoo.addons.calendar.models.calendar_event import get_real_ids class Attachment(models.Model): diff --git a/addons/calendar/models/mail_message.py b/addons/calendar/models/mail_message.py index 58ac99e2825..26f79a68870 100644 --- a/addons/calendar/models/mail_message.py +++ b/addons/calendar/models/mail_message.py @@ -3,7 +3,7 @@ from odoo import api, models -from odoo.addons.calendar.models.calendar import get_real_ids +from odoo.addons.calendar.models.calendar_event import get_real_ids class Message(models.Model): diff --git a/addons/calendar/models/res_partner.py b/addons/calendar/models/res_partner.py index 53448dc0c39..1cb4d8cdb3a 100644 --- a/addons/calendar/models/res_partner.py +++ b/addons/calendar/models/res_partner.py @@ -5,7 +5,7 @@ from datetime import datetime from odoo import api, fields, models -from odoo.addons.calendar.models.calendar import get_real_ids +from odoo.addons.calendar.models.calendar_event import get_real_ids class Partner(models.Model): diff --git a/addons/calendar/tests/test_calendar_recurrent_event_case2.py b/addons/calendar/tests/test_calendar_recurrent_event_case2.py index 632de656ead..131950af172 100644 --- a/addons/calendar/tests/test_calendar_recurrent_event_case2.py +++ b/addons/calendar/tests/test_calendar_recurrent_event_case2.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.tests import common -from odoo.addons.calendar.models.calendar import calendar_id2real_id +from odoo.addons.calendar.models.calendar_event import calendar_id2real_id class TestRecurrentEvent(common.TransactionCase): diff --git a/addons/calendar/wizard/mail_invite.py b/addons/calendar/wizard/mail_invite.py index 96dd29bfdbe..2199827e862 100644 --- a/addons/calendar/wizard/mail_invite.py +++ b/addons/calendar/wizard/mail_invite.py @@ -3,7 +3,7 @@ from odoo import api, models -from odoo.addons.calendar.models.calendar import get_real_ids +from odoo.addons.calendar.models.calendar_event import get_real_ids class MailInvite(models.TransientModel): |
