Initial commit: Technical packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:51 +02:00
commit 3473fa71a0
873 changed files with 297766 additions and 0 deletions

View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import ir_http
from . import res_partner
from . import calendar_event
from . import calendar_alarm
from . import calendar_alarm_manager
from . import calendar_attendee
from . import calendar_filter
from . import calendar_event_type
from . import calendar_recurrence
from . import mail_activity
from . import mail_activity_mixin
from . import mail_activity_type
from . import res_users

View file

@ -0,0 +1,64 @@
# -*- 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'
_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', store=True,
search='_search_duration_minutes', compute='_compute_duration_minutes')
mail_template_id = fields.Many2one(
'mail.template', string="Email Template",
domain=[('model', 'in', ['calendar.attendee'])],
compute='_compute_mail_template_id', readonly=False, store=True,
help="Template used to render mail reminder content.")
body = fields.Text("Additional Message", help="Additional message that would be sent with the notification for the reminder")
@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
@api.depends('alarm_type', 'mail_template_id')
def _compute_mail_template_id(self):
for alarm in self:
if alarm.alarm_type == 'email' and not alarm.mail_template_id:
alarm.mail_template_id = self.env['ir.model.data']._xmlid_to_res_id('calendar.calendar_template_meeting_reminder')
elif alarm.alarm_type != 'email' or not alarm.mail_template_id:
alarm.mail_template_id = False
def _search_duration_minutes(self, operator, value):
return [
'|', '|',
'&', ('interval', '=', 'minutes'), ('duration', operator, value),
'&', ('interval', '=', 'hours'), ('duration', operator, value / 60),
'&', ('interval', '=', 'days'), ('duration', operator, value / 60 / 24),
]
@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)

View file

@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from datetime import timedelta
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
from odoo.tools import plaintext2html
_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, partners=None):
# flush models before making queries
for model_name in ('calendar.alarm', 'calendar.event', 'calendar.recurrence'):
self.env[model_name].flush_model()
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 rrule.until - 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 rrule.until
ELSE cal.stop
END as last_event_date,
calcul_delta.min_delta,
calcul_delta.max_delta,
rrule.rrule AS rule
FROM
calendar_event AS cal
RIGHT JOIN calcul_delta ON calcul_delta.calendar_event_id = cal.id
LEFT JOIN calendar_recurrence as rrule ON rrule.id = cal.recurrence_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 IN %s
"""
# Add filter on alarm type
tuple_params = (alarm_type,)
# Add filter on partner_id
if partners:
base_request += filter_user
tuple_params += (tuple(partners.ids), )
# 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.env.flush_all()
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
past = one_date - timedelta(minutes=(missing * event_maxdelta))
future = fields.Datetime.now() + timedelta(seconds=in_the_next_X_seconds)
if future <= past:
return result
for alarm in event.alarm_ids:
if alarm.alarm_type != alarm_type:
continue
past = one_date - timedelta(minutes=(missing * alarm.duration_minutes))
if future <= past:
continue
if after and past <= fields.Datetime.from_string(after):
continue
result.append({
'alarm_id': alarm.id,
'event_id': event.id,
'notify_at': one_date - timedelta(minutes=alarm.duration_minutes),
})
return result
@api.model
def _get_notify_alert_extra_conditions(self):
"""
To be overriden on inherited modules
adding extra conditions to extract only the unsynced events
"""
return ""
def _get_events_by_alarm_to_notify(self, alarm_type):
"""
Get the events with an alarm of the given type between the cron
last call and now.
Please note that all new reminders created since the cron last
call with an alarm prior to the cron last call are skipped by
design. The attendees receive an invitation for any new event
already.
"""
lastcall = self.env.context.get('lastcall', False) or fields.date.today() - relativedelta(weeks=1)
self.env.cr.execute('''
SELECT "alarm"."id", "event"."id"
FROM "calendar_event" AS "event"
JOIN "calendar_alarm_calendar_event_rel" AS "event_alarm_rel"
ON "event"."id" = "event_alarm_rel"."calendar_event_id"
JOIN "calendar_alarm" AS "alarm"
ON "event_alarm_rel"."calendar_alarm_id" = "alarm"."id"
WHERE
"alarm"."alarm_type" = %s
AND "event"."active"
AND "event"."start" - CAST("alarm"."duration" || ' ' || "alarm"."interval" AS Interval) >= %s
AND "event"."start" - CAST("alarm"."duration" || ' ' || "alarm"."interval" AS Interval) < now() at time zone 'utc'
AND "event"."stop" > now() at time zone 'utc'
''' + self._get_notify_alert_extra_conditions(), [alarm_type, lastcall])
events_by_alarm = {}
for alarm_id, event_id in self.env.cr.fetchall():
events_by_alarm.setdefault(alarm_id, list()).append(event_id)
return events_by_alarm
@api.model
def _send_reminder(self):
# Executed via cron
events_by_alarm = self._get_events_by_alarm_to_notify('email')
if not events_by_alarm:
return
event_ids = list(set(event_id for event_ids in events_by_alarm.values() for event_id in event_ids))
events = self.env['calendar.event'].browse(event_ids)
attendees = events.attendee_ids.filtered(lambda a: a.state != 'declined')
alarms = self.env['calendar.alarm'].browse(events_by_alarm.keys())
for alarm in alarms:
alarm_attendees = attendees.filtered(lambda attendee: attendee.event_id.id in events_by_alarm[alarm.id])
alarm_attendees.with_context(
mail_notify_force_send=True,
calendar_template_ignore_recurrence=True,
mail_notify_author=True
)._send_mail_to_attendees(
alarm.mail_template_id,
force_send=True
)
@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', partners=partner)
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)
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_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
if alarm.body:
message += '<p>%s</p>' % plaintext2html(alarm.body)
delta = alert['notify_at'] - fields.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)),
('groups_id', 'in', self.env.ref('base.group_user').ids),
])
for user in users:
notif = self.with_user(user).with_context(allowed_company_ids=user.company_ids.ids).get_next_notif()
notifications.append([user.partner_id, 'calendar.alarm', notif])
if len(notifications) > 0:
self.env['bus.bus']._sendmany(notifications)

View file

@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import uuid
import base64
import logging
from collections import defaultdict
from odoo import api, Command, fields, models, _
from odoo.addons.base.models.res_partner import _tz_get
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class Attendee(models.Model):
""" Calendar Attendee Information """
_name = 'calendar.attendee'
_rec_name = 'common_name'
_description = 'Calendar Attendee Information'
_order = 'create_date ASC'
def _default_access_token(self):
return uuid.uuid4().hex
STATE_SELECTION = [
('needsAction', 'Needs Action'),
('tentative', 'Uncertain'),
('declined', 'Declined'),
('accepted', 'Accepted'),
]
# event
event_id = fields.Many2one('calendar.event', 'Meeting linked', required=True, ondelete='cascade')
recurrence_id = fields.Many2one('calendar.recurrence', related='event_id.recurrence_id')
# attendee
partner_id = fields.Many2one('res.partner', 'Attendee', required=True, readonly=True)
email = fields.Char('Email', related='partner_id.email')
phone = fields.Char('Phone', related='partner_id.phone')
common_name = fields.Char('Common name', compute='_compute_common_name', store=True)
access_token = fields.Char('Invitation Token', default=_default_access_token)
mail_tz = fields.Selection(_tz_get, compute='_compute_mail_tz', help='Timezone used for displaying time in the mail template')
# state
state = fields.Selection(STATE_SELECTION, string='Status', readonly=True, default='needsAction')
availability = fields.Selection(
[('free', 'Available'), ('busy', 'Busy')], 'Available/Busy', readonly=True)
@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
def _compute_mail_tz(self):
for attendee in self:
attendee.mail_tz = attendee.partner_id.tz
@api.model_create_multi
def create(self, vals_list):
for values in vals_list:
# by default, if no state is given for the attendee corresponding to the current user
# that means he's the event organizer so we can set his state to "accepted"
if 'state' not in values and values.get('partner_id') == self.env.user.partner_id.id:
values['state'] = 'accepted'
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]
values['email'] = email[0] if email else ''
values['common_name'] = values.get("common_name")
attendees = super().create(vals_list)
attendees._subscribe_partner()
return attendees
def unlink(self):
self._unsubscribe_partner()
return super().unlink()
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
raise UserError(_('You cannot duplicate a calendar attendee.'))
def _subscribe_partner(self):
mapped_followers = defaultdict(lambda: self.env['calendar.event'])
for event in self.event_id:
partners = (event.attendee_ids & self).partner_id - event.message_partner_ids
# current user is automatically added as followers, don't add it twice.
partners -= self.env.user.partner_id
mapped_followers[partners] |= event
for partners, events in mapped_followers.items():
events.message_subscribe(partner_ids=partners.ids)
def _unsubscribe_partner(self):
for event in self.event_id:
partners = (event.attendee_ids & self).partner_id & event.message_partner_ids
event.message_unsubscribe(partner_ids=partners.ids)
def _send_mail_to_attendees(self, mail_template, force_send=False):
""" Send mail for event invitation to event attendees.
:param mail_template: a mail.template record
:param force_send: if set to True, the mail(s) will be sent immediately (instead of the next queue processing)
"""
if isinstance(mail_template, str):
raise ValueError('Template should be a template record, not an XML ID anymore.')
if self.env['ir.config_parameter'].sudo().get_param('calendar.block_mail') or self._context.get("no_mail_to_attendees"):
return False
if not mail_template:
_logger.warning("No template passed to %s notification process. Skipped.", self)
return False
# get ics file for all meetings
ics_files = self.mapped('event_id')._get_ics_file()
for attendee in self:
if attendee.email and attendee._should_notify_attendee():
event_id = attendee.event_id.id
ics_file = ics_files.get(event_id)
attachment_values = [Command.set(mail_template.attachment_ids.ids)]
if ics_file:
attachment_values += [
(0, 0, {'name': 'invitation.ics',
'mimetype': 'text/calendar',
'res_id': event_id,
'res_model': 'calendar.event',
'datas': base64.b64encode(ics_file)})
]
body = mail_template._render_field(
'body_html',
attendee.ids,
compute_lang=True,
post_process=True)[attendee.id]
subject = mail_template._render_field(
'subject',
attendee.ids,
compute_lang=True)[attendee.id]
attendee.event_id.with_context(no_document=True).sudo().message_notify(
email_from=attendee.event_id.user_id.email_formatted or self.env.user.email_formatted,
author_id=attendee.event_id.user_id.partner_id.id or self.env.user.partner_id.id,
body=body,
subject=subject,
partner_ids=attendee.partner_id.ids,
email_layout_xmlid='mail.mail_notification_light',
attachment_ids=attachment_values,
force_send=force_send,
)
def _should_notify_attendee(self):
""" Utility method that determines if the attendee should be notified.
By default, we do not want to notify (aka no message and no mail) the current user
if he is part of the attendees.
(Override in appointment to ignore that rule and notify all attendees if it's an appointment)
"""
self.ensure_one()
return self.partner_id != self.env.user.partner_id
def do_tentative(self):
""" Makes event invitation as Tentative. """
return self.write({'state': 'tentative'})
def do_accept(self):
""" Marks event invitation as Accepted. """
for attendee in self:
attendee.event_id.message_post(
author_id=attendee.partner_id.id,
body=_("%s has accepted the invitation") % (attendee.common_name),
subtype_xmlid="calendar.subtype_invitation",
)
return self.write({'state': 'accepted'})
def do_decline(self):
""" Marks event invitation as Declined. """
for attendee in self:
attendee.event_id.message_post(
author_id=attendee.partner_id.id,
body=_("%s has declined the invitation") % (attendee.common_name),
subtype_xmlid="calendar.subtype_invitation",
)
return self.write({'state': 'declined'})

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from random import randint
from odoo import fields, models
class MeetingType(models.Model):
_name = 'calendar.event.type'
_description = 'Event Meeting Type'
def _default_color(self):
return randint(1, 11)
name = fields.Char('Name', required=True)
color = fields.Integer('Color', default=_default_color)
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
]

View file

@ -0,0 +1,22 @@
# -*- 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.filters'
_description = 'Calendar Filters'
user_id = fields.Many2one('res.users', 'Me', required=True, default=lambda self: self.env.user, index=True)
partner_id = fields.Many2one('res.partner', 'Employee', required=True, index=True)
active = fields.Boolean('Active', default=True)
partner_checked = fields.Boolean('Checked', default=True) # used to know if the partner is checked in the filter of the calendar view for the user_id.
_sql_constraints = [
('user_id_partner_id_unique', 'UNIQUE(user_id, partner_id)', 'A user cannot have the same contact twice.')
]
@api.model
def unlink_from_partner_id(self, partner_id):
return self.search([('partner_id', '=', partner_id)]).unlink()

View file

@ -0,0 +1,576 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, time
import pytz
import re
from dateutil import rrule
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.addons.base.models.res_partner import _tz_get
MAX_RECURRENT_EVENT = 720
SELECT_FREQ_TO_RRULE = {
'daily': rrule.DAILY,
'weekly': rrule.WEEKLY,
'monthly': rrule.MONTHLY,
'yearly': rrule.YEARLY,
}
RRULE_FREQ_TO_SELECT = {
rrule.DAILY: 'daily',
rrule.WEEKLY: 'weekly',
rrule.MONTHLY: 'monthly',
rrule.YEARLY: 'yearly',
}
RRULE_WEEKDAY_TO_FIELD = {
rrule.MO.weekday: 'mon',
rrule.TU.weekday: 'tue',
rrule.WE.weekday: 'wed',
rrule.TH.weekday: 'thu',
rrule.FR.weekday: 'fri',
rrule.SA.weekday: 'sat',
rrule.SU.weekday: 'sun',
}
RRULE_WEEKDAYS = {'SUN': 'SU', 'MON': 'MO', 'TUE': 'TU', 'WED': 'WE', 'THU': 'TH', 'FRI': 'FR', 'SAT': 'SA'}
RRULE_TYPE_SELECTION = [
('daily', 'Days'),
('weekly', 'Weeks'),
('monthly', 'Months'),
('yearly', 'Years'),
]
END_TYPE_SELECTION = [
('count', 'Number of repetitions'),
('end_date', 'End date'),
('forever', 'Forever'),
]
MONTH_BY_SELECTION = [
('date', 'Date of month'),
('day', 'Day of month'),
]
WEEKDAY_SELECTION = [
('MON', 'Monday'),
('TUE', 'Tuesday'),
('WED', 'Wednesday'),
('THU', 'Thursday'),
('FRI', 'Friday'),
('SAT', 'Saturday'),
('SUN', 'Sunday'),
]
BYDAY_SELECTION = [
('1', 'First'),
('2', 'Second'),
('3', 'Third'),
('4', 'Fourth'),
('-1', 'Last'),
]
def freq_to_select(rrule_freq):
return RRULE_FREQ_TO_SELECT[rrule_freq]
def freq_to_rrule(freq):
return SELECT_FREQ_TO_RRULE[freq]
def weekday_to_field(weekday_index):
return RRULE_WEEKDAY_TO_FIELD.get(weekday_index)
class RecurrenceRule(models.Model):
_name = 'calendar.recurrence'
_description = 'Event Recurrence Rule'
name = fields.Char(compute='_compute_name', store=True)
base_event_id = fields.Many2one(
'calendar.event', ondelete='set null', copy=False) # store=False ?
calendar_event_ids = fields.One2many('calendar.event', 'recurrence_id')
event_tz = fields.Selection(
_tz_get, string='Timezone',
default=lambda self: self.env.context.get('tz') or self.env.user.tz)
rrule = fields.Char(compute='_compute_rrule', inverse='_inverse_rrule', store=True)
dtstart = fields.Datetime(compute='_compute_dtstart')
rrule_type = fields.Selection(RRULE_TYPE_SELECTION, default='weekly')
end_type = fields.Selection(END_TYPE_SELECTION, default='count')
interval = fields.Integer(default=1)
count = fields.Integer(default=1)
mon = fields.Boolean()
tue = fields.Boolean()
wed = fields.Boolean()
thu = fields.Boolean()
fri = fields.Boolean()
sat = fields.Boolean()
sun = fields.Boolean()
month_by = fields.Selection(MONTH_BY_SELECTION, default='date')
day = fields.Integer(default=1)
weekday = fields.Selection(WEEKDAY_SELECTION, string='Weekday')
byday = fields.Selection(BYDAY_SELECTION, string='By day')
until = fields.Date('Repeat Until')
_sql_constraints = [
('month_day',
"CHECK (rrule_type != 'monthly' "
"OR month_by != 'day' "
"OR day >= 1 AND day <= 31 "
"OR weekday in %s AND byday in %s)"
% (tuple(wd[0] for wd in WEEKDAY_SELECTION), tuple(bd[0] for bd in BYDAY_SELECTION)),
"The day must be between 1 and 31"),
]
def _get_daily_recurrence_name(self):
if self.end_type == 'count':
return _("Every %(interval)s Days for %(count)s events", interval=self.interval, count=self.count)
if self.end_type == 'end_date':
return _("Every %(interval)s Days until %(until)s", interval=self.interval, until=self.until)
return _("Every %(interval)s Days", interval=self.interval)
def _get_weekly_recurrence_name(self):
weekday_selection = dict(self._fields['weekday']._description_selection(self.env))
weekdays = self._get_week_days()
# Convert Weekday object
weekdays = [str(w) for w in weekdays]
# We need to get the day full name from its three first letters.
week_map = {v: k for k, v in RRULE_WEEKDAYS.items()}
weekday_short = [week_map[w] for w in weekdays]
day_strings = [weekday_selection[day] for day in weekday_short]
days = ", ".join(day_strings)
if self.end_type == 'count':
return _("Every %(interval)s Weeks on %(days)s for %(count)s events", interval=self.interval, days=days, count=self.count)
if self.end_type == 'end_date':
return _("Every %(interval)s Weeks on %(days)s until %(until)s", interval=self.interval, days=days, until=self.until)
return _("Every %(interval)s Weeks on %(days)s", interval=self.interval, days=days)
def _get_monthly_recurrence_name(self):
if self.month_by == 'day':
weekday_selection = dict(self._fields['weekday']._description_selection(self.env))
byday_selection = dict(self._fields['byday']._description_selection(self.env))
position_label = byday_selection[self.byday]
weekday_label = weekday_selection[self.weekday]
if self.end_type == 'count':
return _("Every %(interval)s Months on the %(position)s %(weekday)s for %(count)s events", interval=self.interval, position=position_label, weekday=weekday_label, count=self.count)
if self.end_type == 'end_date':
return _("Every %(interval)s Months on the %(position)s %(weekday)s until %(until)s", interval=self.interval, position=position_label, weekday=weekday_label, until=self.until)
return _("Every %(interval)s Months on the %(position)s %(weekday)s", interval=self.interval, position=position_label, weekday=weekday_label)
else:
if self.end_type == 'count':
return _("Every %(interval)s Months day %(day)s for %(count)s events", interval=self.interval, day=self.day, count=self.count)
if self.end_type == 'end_date':
return _("Every %(interval)s Months day %(day)s until %(until)s", interval=self.interval, day=self.day, until=self.until)
return _("Every %(interval)s Months day %(day)s", interval=self.interval, day=self.day)
def _get_yearly_recurrence_name(self):
if self.end_type == 'count':
return _("Every %(interval)s Years for %(count)s events", interval=self.interval, count=self.count)
if self.end_type == 'end_date':
return _("Every %(interval)s Years until %(until)s", interval=self.interval, until=self.until)
return _("Every %(interval)s Years", interval=self.interval)
def get_recurrence_name(self):
if self.rrule_type == 'daily':
return self._get_daily_recurrence_name()
if self.rrule_type == 'weekly':
return self._get_weekly_recurrence_name()
if self.rrule_type == 'monthly':
return self._get_monthly_recurrence_name()
if self.rrule_type == 'yearly':
return self._get_yearly_recurrence_name()
@api.depends('rrule')
def _compute_name(self):
for recurrence in self:
recurrence.name = recurrence.get_recurrence_name()
@api.depends('calendar_event_ids.start')
def _compute_dtstart(self):
groups = self.env['calendar.event'].read_group([('recurrence_id', 'in', self.ids)], ['start:min'], ['recurrence_id'])
start_mapping = {
group['recurrence_id'][0]: group['start']
for group in groups
}
for recurrence in self:
recurrence.dtstart = start_mapping.get(recurrence.id)
@api.depends(
'byday', 'until', 'rrule_type', 'month_by', 'interval', 'count', 'end_type',
'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'day', 'weekday')
def _compute_rrule(self):
for recurrence in self:
current_rule = recurrence._rrule_serialize()
if recurrence.rrule != current_rule:
recurrence.write({'rrule': current_rule})
def _inverse_rrule(self):
for recurrence in self:
if recurrence.rrule:
values = self._rrule_parse(recurrence.rrule, recurrence.dtstart)
recurrence.with_context(dont_notify=True).write(values)
def _reconcile_events(self, ranges):
"""
:param ranges: iterable of tuples (datetime_start, datetime_stop)
:return: tuple (events of the recurrence already in sync with ranges,
and ranges not covered by any events)
"""
ranges = set(ranges)
synced_events = self.calendar_event_ids.filtered(lambda e: e._range() in ranges)
existing_ranges = set(event._range() for event in synced_events)
ranges_to_create = (event_range for event_range in ranges if event_range not in existing_ranges)
return synced_events, ranges_to_create
def _select_new_base_event(self):
"""
when the base event is no more available (archived, deleted, etc.), a new one should be selected
"""
for recurrence in self:
recurrence.base_event_id = recurrence._get_first_event()
def _apply_recurrence(self, specific_values_creation=None, no_send_edit=False, generic_values_creation=None):
"""Create missing events in the recurrence and detach events which no longer
follow the recurrence rules.
:return: detached events
"""
event_vals = []
keep = self.env['calendar.event']
if specific_values_creation is None:
specific_values_creation = {}
for recurrence in self.filtered('base_event_id'):
recurrence.calendar_event_ids |= recurrence.base_event_id
event = recurrence.base_event_id or recurrence._get_first_event(include_outliers=False)
duration = event.stop - event.start
if specific_values_creation:
ranges = set([(x[1], x[2]) for x in specific_values_creation if x[0] == recurrence.id])
else:
ranges = recurrence._range_calculation(event, duration)
events_to_keep, ranges = recurrence._reconcile_events(ranges)
keep |= events_to_keep
[base_values] = event.copy_data()
values = []
for start, stop in ranges:
value = dict(base_values, start=start, stop=stop, recurrence_id=recurrence.id, follow_recurrence=True)
if (recurrence.id, start, stop) in specific_values_creation:
value.update(specific_values_creation[(recurrence.id, start, stop)])
if generic_values_creation and recurrence.id in generic_values_creation:
value.update(generic_values_creation[recurrence.id])
values += [value]
event_vals += values
events = self.calendar_event_ids - keep
detached_events = self._detach_events(events)
self.env['calendar.event'].with_context(no_mail_to_attendees=True, mail_create_nolog=True).create(event_vals)
return detached_events
def _split_from(self, event, recurrence_values=None):
"""Stops the current recurrence at the given event and creates a new one starting
with the event.
:param event: starting point of the new recurrence
:param recurrence_values: values applied to the new recurrence
:return: new recurrence
"""
if recurrence_values is None:
recurrence_values = {}
event.ensure_one()
if not self:
return
[values] = self.copy_data()
detached_events = self._stop_at(event)
count = recurrence_values.get('count', 0) or len(detached_events)
return self.create({
**values,
**recurrence_values,
'base_event_id': event.id,
'calendar_event_ids': [(6, 0, detached_events.ids)],
'count': max(count, 1),
})
def _stop_at(self, event):
"""Stops the recurrence at the given event. Detach the event and all following
events from the recurrence.
:return: detached events from the recurrence
"""
self.ensure_one()
events = self._get_events_from(event.start)
detached_events = self._detach_events(events)
if not self.calendar_event_ids:
self.with_context(archive_on_error=True).unlink()
return detached_events
if event.allday:
until = self._get_start_of_period(event.start_date)
else:
until_datetime = self._get_start_of_period(event.start)
until_timezoned = pytz.utc.localize(until_datetime).astimezone(self._get_timezone())
until = until_timezoned.date()
self.write({
'end_type': 'end_date',
'until': until - relativedelta(days=1),
})
return detached_events
@api.model
def _detach_events(self, events):
events.with_context(dont_notify=True).write({
'recurrence_id': False,
'recurrency': False,
})
return events
def _write_events(self, values, dtstart=None):
"""
Write values on events in the recurrence.
:param values: event values
:param dstart: if provided, only write events starting from this point in time
"""
events = self._get_events_from(dtstart) if dtstart else self.calendar_event_ids
return events.with_context(no_mail_to_attendees=True, dont_notify=True).write(dict(values, recurrence_update='self_only'))
def _rrule_serialize(self):
"""
Compute rule string according to value type RECUR of iCalendar
:return: string containing recurring rule (empty if no rule)
"""
if self.interval <= 0:
raise UserError(_('The interval cannot be negative.'))
if self.end_type == 'count' and self.count <= 0:
raise UserError(_('The number of repetitions cannot be negative.'))
return str(self._get_rrule()) if self.rrule_type else ''
@api.model
def _rrule_parse(self, rule_str, date_start):
# LUL TODO clean this mess
data = {}
day_list = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
# Skip X-named RRULE extensions
# TODO Remove patch when dateutils contains the fix
# HACK https://github.com/dateutil/dateutil/pull/1374
# Optional parameters starts with X- and they can be placed anywhere in the RRULE string.
# RRULE:FREQ=MONTHLY;INTERVAL=3;X-RELATIVE=1
# RRULE;X-EVOLUTION-ENDDATE=20200120:FREQ=WEEKLY;COUNT=3;BYDAY=MO
# X-EVOLUTION-ENDDATE=20200120:FREQ=WEEKLY;COUNT=3;BYDAY=MO
rule_str = re.sub(r';?X-[-\w]+=[^;:]*', '', rule_str).replace(":;", ":").lstrip(":;")
if 'Z' in rule_str and date_start and not date_start.tzinfo:
date_start = pytz.utc.localize(date_start)
rule = rrule.rrulestr(rule_str, dtstart=date_start)
data['rrule_type'] = freq_to_select(rule._freq)
data['count'] = rule._count
data['interval'] = rule._interval
data['until'] = rule._until
# Repeat weekly
if rule._byweekday:
for weekday in day_list:
data[weekday] = False # reset
for weekday_index in rule._byweekday:
weekday = rrule.weekday(weekday_index)
data[weekday_to_field(weekday.weekday)] = True
data['rrule_type'] = 'weekly'
# Repeat monthly by nweekday ((weekday, weeknumber), )
if rule._bynweekday:
data['weekday'] = day_list[list(rule._bynweekday)[0][0]].upper()
data['byday'] = str(list(rule._bynweekday)[0][1])
data['month_by'] = 'day'
data['rrule_type'] = 'monthly'
if rule._bymonthday:
data['day'] = list(rule._bymonthday)[0]
data['month_by'] = 'date'
data['rrule_type'] = 'monthly'
# Repeat yearly but for odoo it's monthly, take same information as monthly but interval is 12 times
if rule._bymonth:
data['interval'] *= 12
if data.get('until'):
data['end_type'] = 'end_date'
elif data.get('count'):
data['end_type'] = 'count'
else:
data['end_type'] = 'forever'
return data
def _get_lang_week_start(self):
lang = self.env['res.lang']._lang_get(self.env.user.lang)
week_start = int(lang.week_start) # lang.week_start ranges from '1' to '7'
return rrule.weekday(week_start - 1) # rrule expects an int from 0 to 6
def _get_start_of_period(self, dt):
if self.rrule_type == 'weekly':
week_start = self._get_lang_week_start()
start = dt + relativedelta(weekday=week_start(-1))
elif self.rrule_type == 'monthly':
start = dt + relativedelta(day=1)
else:
start = dt
# Comparaison of DST (to manage the case of going too far back in time).
# If we detect a change in the DST between the creation date of an event
# and the date used for the occurrence period, we use the creation date of the event.
# This is a hack to avoid duplication of events (for example on google calendar).
if isinstance(dt, datetime):
timezone = self._get_timezone()
dst_dt = timezone.localize(dt).dst()
dst_start = timezone.localize(start).dst()
if dst_dt != dst_start:
start = dt
return start
def _get_first_event(self, include_outliers=False):
if not self.calendar_event_ids:
return self.env['calendar.event']
events = self.calendar_event_ids.sorted('start')
if not include_outliers:
events -= self._get_outliers()
return events[:1]
def _get_outliers(self):
synced_events = self.env['calendar.event']
for recurrence in self:
if recurrence.calendar_event_ids:
start = min(recurrence.calendar_event_ids.mapped('start'))
starts = set(recurrence._get_occurrences(start))
synced_events |= recurrence.calendar_event_ids.filtered(lambda e: e.start in starts)
return self.calendar_event_ids - synced_events
def _range_calculation(self, event, duration):
""" Calculate the range of recurrence when applying the recurrence
The following issues are taken into account:
start of period is sometimes in the past (weekly or monthly rule).
We can easily filter these range values but then the count value may be wrong...
In that case, we just increase the count value, recompute the ranges and dismiss the useless values
"""
self.ensure_one()
original_count = self.end_type == 'count' and self.count
ranges = set(self._get_ranges(event.start, duration))
future_events = set((x, y) for x, y in ranges if x.date() >= event.start.date() and y.date() >= event.start.date())
if original_count and len(future_events) < original_count:
# Rise count number because some past values will be dismissed.
self.count = (2*original_count) - len(future_events)
ranges = set(self._get_ranges(event.start, duration))
# We set back the occurrence number to its original value
self.count = original_count
# Remove ranges of events occurring in the past
ranges = set((x, y) for x, y in ranges if x.date() >= event.start.date() and y.date() >= event.start.date())
return ranges
def _get_ranges(self, start, event_duration):
starts = self._get_occurrences(start)
return ((start, start + event_duration) for start in starts)
def _get_timezone(self):
return pytz.timezone(self.event_tz or self.env.context.get('tz') or 'UTC')
def _get_occurrences(self, dtstart):
"""
Get ocurrences of the rrule
:param dtstart: start of the recurrence
:return: iterable of datetimes
"""
self.ensure_one()
dtstart = self._get_start_of_period(dtstart)
if self._is_allday():
return self._get_rrule(dtstart=dtstart)
timezone = self._get_timezone()
# Localize the starting datetime to avoid missing the first occurrence
dtstart = pytz.utc.localize(dtstart).astimezone(timezone)
# dtstart is given as a naive datetime, but it actually represents a timezoned datetime
# (rrule package expects a naive datetime)
occurences = self._get_rrule(dtstart=dtstart.replace(tzinfo=None))
# Special timezoning is needed to handle DST (Daylight Saving Time) changes.
# Given the following recurrence:
# - monthly
# - 1st of each month
# - timezone America/New_York (UTC05:00)
# - at 6am America/New_York = 11am UTC
# - from 2019/02/01 to 2019/05/01.
# The naive way would be to store:
# 2019/02/01 11:00 - 2019/03/01 11:00 - 2019/04/01 11:00 - 2019/05/01 11:00 (UTC)
#
# But a DST change occurs on 2019/03/10 in America/New_York timezone. America/New_York is now UTC04:00.
# From this point in time, 11am (UTC) is actually converted to 7am (America/New_York) instead of the expected 6am!
# What should be stored is:
# 2019/02/01 11:00 - 2019/03/01 11:00 - 2019/04/01 10:00 - 2019/05/01 10:00 (UTC)
# ***** *****
return (timezone.localize(occurrence, is_dst=False).astimezone(pytz.utc).replace(tzinfo=None) for occurrence in occurences)
def _get_events_from(self, dtstart):
return self.env['calendar.event'].search([
('id', 'in', self.calendar_event_ids.ids),
('start', '>=', dtstart)
])
def _get_week_days(self):
"""
:return: tuple of rrule weekdays for this recurrence.
"""
return tuple(
rrule.weekday(weekday_index)
for weekday_index, weekday in {
rrule.MO.weekday: self.mon,
rrule.TU.weekday: self.tue,
rrule.WE.weekday: self.wed,
rrule.TH.weekday: self.thu,
rrule.FR.weekday: self.fri,
rrule.SA.weekday: self.sat,
rrule.SU.weekday: self.sun,
}.items() if weekday
)
def _is_allday(self):
"""Returns whether a majority of events are allday or not (there might be some outlier events)
"""
score = sum(1 if e.allday else -1 for e in self.calendar_event_ids)
return score >= 0
def _get_rrule(self, dtstart=None):
self.ensure_one()
freq = self.rrule_type
rrule_params = dict(
dtstart=dtstart,
interval=self.interval,
)
if freq == 'monthly' and self.month_by == 'date': # e.g. every 15th of the month
rrule_params['bymonthday'] = self.day
elif freq == 'monthly' and self.month_by == 'day': # e.g. every 2nd Monday in the month
rrule_params['byweekday'] = getattr(rrule, RRULE_WEEKDAYS[self.weekday])(int(self.byday)) # e.g. MO(+2) for the second Monday of the month
elif freq == 'weekly':
weekdays = self._get_week_days()
if not weekdays:
raise UserError(_("You have to choose at least one day in the week"))
rrule_params['byweekday'] = weekdays
rrule_params['wkst'] = self._get_lang_week_start()
if self.end_type == 'count': # e.g. stop after X occurence
rrule_params['count'] = min(self.count, MAX_RECURRENT_EVENT)
elif self.end_type == 'forever':
rrule_params['count'] = MAX_RECURRENT_EVENT
elif self.end_type == 'end_date': # e.g. stop after 12/10/2020
rrule_params['until'] = datetime.combine(self.until, time.max)
return rrule.rrule(
freq_to_rrule(freq), **rrule_params
)

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.exceptions import BadRequest
from odoo import models
from odoo.http import request
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
@classmethod
def _auth_method_calendar(cls):
token = request.get_http_params().get('token', '')
error_message = False
attendee = request.env['calendar.attendee'].sudo().search([('access_token', '=', token)], limit=1)
if not attendee:
error_message = """Invalid Invitation Token."""
elif request.session.uid and request.session.login != 'anonymous':
# if valid session but user is not match
user = request.env['res.users'].sudo().browse(request.session.uid)
if attendee.partner_id != user.partner_id:
error_message = """Invitation cannot be forwarded via email. This event/meeting belongs to %s and you are logged in as %s. Please ask organizer to add you.""" % (attendee.email, user.email)
if error_message:
raise BadRequest(error_message)
cls._auth_method_public()

View file

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, tools, _
from odoo.tools import is_html_empty
class MailActivity(models.Model):
_inherit = "mail.activity"
calendar_event_id = fields.Many2one('calendar.event', string="Calendar Meeting", ondelete='cascade')
def action_create_calendar_event(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("calendar.action_calendar_event")
action['context'] = {
'default_activity_type_id': self.activity_type_id.id,
'default_res_id': self.env.context.get('default_res_id'),
'default_res_model': self.env.context.get('default_res_model'),
'default_name': self.summary or self.res_name,
'default_description': self.note if not is_html_empty(self.note) else '',
'default_activity_ids': [(6, 0, self.ids)],
'orig_activity_ids': self,
}
return action
def _action_done(self, feedback=False, attachment_ids=False):
events = self.calendar_event_id
# To avoid the feedback to be included in the activity note (due to the synchronization in event.write
# that updates the related activity note each time the event description is updated),
# when the activity is written as a note in the chatter in _action_done (leading to duplicate feedback),
# we call super before updating the description. As self is deleted in super, we load the related events before.
messages, activities = super(MailActivity, self)._action_done(feedback=feedback, attachment_ids=attachment_ids)
if feedback:
for event in events:
description = event.description
description = '%s<br />%s' % (
description if not tools.is_html_empty(description) else '',
_('Feedback: %(feedback)s', feedback=tools.plaintext2html(feedback)) if feedback else '',
)
event.write({'description': description})
return messages, activities
def unlink_w_meeting(self):
events = self.mapped('calendar_event_id')
res = self.unlink()
events.unlink()
return res

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class MailActivityMixin(models.AbstractModel):
_inherit = 'mail.activity.mixin'
activity_calendar_event_id = fields.Many2one(
'calendar.event', string="Next Activity Calendar Event",
compute='_compute_activity_calendar_event_id', groups="base.group_user")
@api.depends('activity_ids.calendar_event_id')
def _compute_activity_calendar_event_id(self):
"""This computes the calendar event of the next activity.
It evaluates to false if there is no such event."""
for record in self:
record.activity_calendar_event_id = fields.first(record.activity_ids).calendar_event_id

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class MailActivityType(models.Model):
_inherit = "mail.activity.type"
category = fields.Selection(selection_add=[('meeting', 'Meeting')])

View file

@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from odoo import api, fields, models
class Partner(models.Model):
_inherit = 'res.partner'
meeting_count = fields.Integer("# Meetings", compute='_compute_meeting_count')
meeting_ids = fields.Many2many('calendar.event', 'calendar_event_res_partner_rel', 'res_partner_id',
'calendar_event_id', string='Meetings', copy=False)
calendar_last_notif_ack = fields.Datetime(
'Last notification marked as read from base Calendar', default=fields.Datetime.now)
def _compute_meeting_count(self):
result = self._compute_meeting()
for p in self:
p.meeting_count = len(result.get(p.id, []))
def _compute_meeting(self):
if self.ids:
all_partners = self.with_context(active_test=False).search([('id', 'child_of', self.ids)])
event_id = self.env['calendar.event']._search([]) # ir.rules will be applied
subquery_string, subquery_params = event_id.select()
subquery = self.env.cr.mogrify(subquery_string, subquery_params).decode()
self.env.cr.execute("""
SELECT res_partner_id, calendar_event_id, count(1)
FROM calendar_event_res_partner_rel
WHERE res_partner_id IN %s AND calendar_event_id IN ({})
GROUP BY res_partner_id, calendar_event_id
""".format(subquery), [tuple(p["id"] for p in all_partners)])
meeting_data = self.env.cr.fetchall()
# Create a dict {partner_id: event_ids} and fill with events linked to the partner
meetings = {}
for p_id, m_id, _ in meeting_data:
meetings.setdefault(p_id, set()).add(m_id)
# Add the events linked to the children of the partner
for meeting_pid in set(meetings):
partner = self.browse(meeting_pid)
while partner:
partner = partner.parent_id
if partner in self:
meetings[partner.id] = meetings.get(partner.id, set()) | meetings[meeting_pid]
return {p_id: list(meetings.get(p_id, set())) for p_id in self.ids}
return {}
def get_attendee_detail(self, meeting_ids):
""" Return a list of dict of the given meetings with the attendees details
Used by:
- base_calendar.js : Many2ManyAttendee
- calendar_model.js (calendar.CalendarModel)
"""
attendees_details = []
meetings = self.env['calendar.event'].browse(meeting_ids)
for attendee in meetings.attendee_ids:
if attendee.partner_id not in self:
continue
attendee_is_organizer = self.env.user == attendee.event_id.user_id and attendee.partner_id == self.env.user.partner_id
attendees_details.append({
'id': attendee.partner_id.id,
'name': attendee.partner_id.display_name,
'status': attendee.state,
'event_id': attendee.event_id.id,
'attendee_id': attendee.id,
'is_alone': attendee.event_id.is_organizer_alone and attendee_is_organizer,
# attendees data is sorted according to this key in JS.
'is_organizer': 1 if attendee.partner_id == attendee.event_id.user_id.partner_id else 0,
})
return attendees_details
@api.model
def _set_calendar_last_notif_ack(self):
partner = self.env['res.users'].browse(self.env.context.get('uid', self.env.uid)).partner_id
partner.write({'calendar_last_notif_ack': datetime.now()})
def schedule_meeting(self):
self.ensure_one()
partner_ids = self.ids
partner_ids.append(self.env.user.partner_id.id)
action = self.env["ir.actions.actions"]._for_xml_id("calendar.action_calendar_event")
action['context'] = {
'default_partner_ids': partner_ids,
}
action['domain'] = ['|', ('id', 'in', self._compute_meeting()[self.id]), ('partner_ids', 'in', self.ids)]
return action

View file

@ -0,0 +1,83 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from odoo import api, fields, models, modules, _
from pytz import timezone, UTC
class Users(models.Model):
_inherit = 'res.users'
def _systray_get_calendar_event_domain(self):
# Determine the domain for which the users should be notified. This method sends notification to
# events occurring between now and the end of the day. Note that "now" needs to be computed in the
# user TZ and converted into UTC to compare with the records values and "the end of the day" needs
# also conversion. Otherwise TZ diverting a lot from UTC would send notification for events occurring
# tomorrow.
# The user is notified if the start is occurring between now and the end of the day
# if the event is not finished.
# | |
# |===========|===> DAY A (`start_dt`): now in the user TZ
# | |
# | | <--- `start_dt_utc`: now is on the right if the user lives
# | | in West Longitude (America for example)
# | |
# | ------- | <--- `start`: the start of the event (in UTC)
# | | event | |
# | ------- | <--- `stop`: the stop of the event (in UTC)
# | |
# | |
# | | <--- `stop_dt_utc` = `stop_dt` if user lives in an area of East longitude (positive shift compared to UTC, Belgium for example)
# | |
# | |
# |-----------| <--- `stop_dt` = end of the day for DAY A from user point of view (23:59 in this TZ)
# |===========|===> DAY B
# | |
# | | <--- `stop_dt_utc` = `stop_dt` if user lives in an area of West longitude (positive shift compared to UTC, America for example)
# | |
start_dt_utc = start_dt = datetime.datetime.now(UTC)
stop_dt_utc = UTC.localize(datetime.datetime.combine(start_dt.date(), datetime.time.max))
tz = self.env.user.tz
if tz:
user_tz = timezone(tz)
start_dt = start_dt_utc.astimezone(user_tz)
stop_dt = user_tz.localize(datetime.datetime.combine(start_dt.date(), datetime.time.max))
stop_dt_utc = stop_dt.astimezone(UTC)
start_date = start_dt.date()
return ['&', '|',
'&',
'|',
['start', '>=', fields.Datetime.to_string(start_dt_utc)],
['stop', '>=', fields.Datetime.to_string(start_dt_utc)],
['start', '<=', fields.Datetime.to_string(stop_dt_utc)],
'&',
['allday', '=', True],
['start_date', '=', fields.Date.to_string(start_date)],
('attendee_ids.partner_id', '=', self.env.user.partner_id.id)]
@api.model
def systray_get_activities(self):
res = super(Users, self).systray_get_activities()
meetings_lines = self.env['calendar.event'].search_read(
self._systray_get_calendar_event_domain(),
['id', 'start', 'name', 'allday', 'attendee_status'],
order='start')
meetings_lines = [line for line in meetings_lines if line['attendee_status'] != 'declined']
if meetings_lines:
meeting_label = _("Today's Meetings")
meetings_systray = {
'id': self.env['ir.model']._get('calendar.event').id,
'type': 'meeting',
'name': meeting_label,
'model': 'calendar.event',
'icon': modules.module.get_module_icon(self.env['calendar.event']._original_module),
'meetings': meetings_lines,
}
res.insert(0, meetings_systray)
return res