summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorLucas Lefèvre <lul@odoo.com>2019-11-26 13:24:14 +0100
committerLucas Lefèvre <lul@odoo.com>2020-04-09 10:53:46 +0200
commit779912007441d005641daadbd8336029950cc7d0 (patch)
tree6a7bb75429d1184d680974ee926485391c23bb2e /addons
parent60b5220c7230464157f21a8a3fc12c909ca7eb79 (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__.py8
-rw-r--r--addons/calendar/models/calendar_alarm.py58
-rw-r--r--addons/calendar/models/calendar_alarm_manager.py247
-rw-r--r--addons/calendar/models/calendar_attendee.py139
-rw-r--r--addons/calendar/models/calendar_contact.py21
-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.py16
-rw-r--r--addons/calendar/models/ir_attachment.py2
-rw-r--r--addons/calendar/models/mail_message.py2
-rw-r--r--addons/calendar/models/res_partner.py2
-rw-r--r--addons/calendar/tests/test_calendar_recurrent_event_case2.py2
-rw-r--r--addons/calendar/wizard/mail_invite.py2
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):