[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
This commit is contained in:
Lucas Lefèvre
2020-04-09 10:53:46 +02:00
parent 60b5220c72
commit 7799120074
12 changed files with 496 additions and 461 deletions
+7 -1
View File
@@ -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
+58
View File
@@ -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
@@ -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)
+139
View File
@@ -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
@@ -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()
@@ -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
@@ -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 !"),
]
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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):
@@ -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):
+1 -1
View File
@@ -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):