[REF] calendar: Materialize virtual recurring events

TL;DR A bit of magic and ... Pouf ... all virtual events are now real records.

Purpose
=======
The current implementation of recurring events uses "virtual events" to represents all
events in the recurrence. Only one event is stored in the database. All other events are
dynamically built and sent to the web client which sees them as real records.
This implementation is old, difficult to read (6 years of changes and bug fixes) and
has become more and more difficult to maintain.
A fresh start would be welcome.

Specification
=============
When creating a recurring event, a `calendar.recurrence.rule` record should be created.
This model holds the rrule configuration and is responsible to manage events that result
from the rrule.
When a new recurrence is created, all resulting events are also created (stored in the
database). No more virtual events.
To avoid an explosion of events, a maximum of 720 events is created. With 720 events,
a daily recurrence lasts for 2 years and a weekly recurrence lasts for 15 years.
This strategy is used by Google Calendar and seems acceptable in most cases.
A cron job could eventually be introduced to generated more events as the end comes.

This allows to introduce an new rrule end type: 'Forever'. This is actually a shortcut
to create a recurrence running for 720 events.

Inspired by Google Calendar, new possibilities are introduced when modifying a recurring
event (e.g. change the name, add an attendee):
- Modify only this particular event (the event is still part of the recurrence)
- Modify this and following events (events are still in the same recurrence)
- Modify all events

In a similar way, when updating the rrule of an event (e.g. from every Monday to every
Tuesday), the user can choose to:
- Modify this and following events: the recurrence is split. The first part remains
  unchanged but now ends when the second recurrence begins. The second parts is the
  updated rrule.
- Modify only this particular event.
- Modify all events: Forbidden (same as Google Calendar)

Note: when events are "moved" because the rrule changed, the events are not actually
moved. They are unlinked and new events are created (See "Some design choices explained"
section).

Some design choices explained
=============================

Where to store the rrule?
-------------------------
Two options were considered.
1) Store the rrule on an event, each event of the recurrence having a Many2one
to the parent_id, aka the Master Event of the recurrence.

2) Store the rrule on another model: `calendar.recurrence.rule`. Each event in the
recurrence having a Many2one to the recurrence record.

Both options have pros & cons. But there is no clear winner.
However the actual business logic of handling the recurrence creation/update
would be the same.
Where the rrule configuration is stored is the only main difference between both options.
And this is probably the easiest part of implementing recurring events.

With that in mind, option 2) is chosen. It allows to clearly separate recurrence
logic from the events themselves. It is also the "correct" way of modeling data to avoid
many empty columns for most records.

Reusing events on rrule update
------------------------------
When an rrule is modified, events should also be updated.
In most cases, current events are unlinked and new events are created from scratch.
Only events exactly at the same time before and after the rrule update are kept.
Trying to reuse other events would require an obscure and arbitrary heuristic.
(Imagine an rrule every Monday that is changed to every Tuesday and every Friday.
What would you do?).
This implies that any change to a specific event (name change, attendee added, chatter
messages) is lost. This tradeoff seems acceptable as updating an rrule should not be
that frequent. Moreover, Google Calendar also works that way so why not Odoo?

Reusing events on events shifts
-------------------------------
On the calendar view, drag and drop an event to shift the entire recurrence.

One way to handle the recurrence shift is to apply the same timedelta to all events.
Now events are correctly positioned... except for events that were specifically
moved. Those outliers needs special handling.
This also introduces some behavior inconsistencies:
when an rrule is directly modified, events are not reused, but when the rrule is
modified by drag & dropping an event, events are reused. This option needs more code to
handle the shift and the behaviors are not consistent.

The chosen option is to find the rrule configuration from the dragged event (this is
easy) and update the recurrence with those new rrule values. This brings us back to
an rrule update (see above): code reusability yeah; one behavior to rule them all yeah.
One downside is that outliers are lost (but Google Calendar also works that way).

Task 2126717
PR #42031
PR Enterprise odoo/enterprise#8006
This commit is contained in:
Lucas Lefèvre
2020-04-09 10:56:37 +02:00
parent 7799120074
commit a27afdb543
25 changed files with 1439 additions and 1206 deletions
-1
View File
@@ -3,4 +3,3 @@
from . import controllers
from . import models
from . import wizard
+1 -2
View File
@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import ir_attachment
from . import ir_http
from . import res_partner
from . import mail_message
from . import calendar_event
from . import calendar_alarm
from . import calendar_alarm_manager
@@ -12,5 +10,6 @@ from . import calendar_attendee
from . import calendar_contact
from . import calendar_event_type
from . import calendar_event
from . import calendar_recurrence
from . import mail_activity
from . import res_users
@@ -31,20 +31,21 @@ class AlarmManager(models.AbstractModel):
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
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 cal.final_date
WHEN cal.recurrency THEN rrule.until
ELSE cal.stop
END as last_event_date,
calcul_delta.min_delta,
calcul_delta.max_delta,
cal.rrule AS rule
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 = """
@@ -157,10 +158,10 @@ class AlarmManager(models.AbstractModel):
for meeting in self.env['calendar.event'].browse(all_meetings):
max_delta = all_meetings[meeting.id]['max_duration']
if meeting.recurrency:
if meeting.recurrency and meeting.recurrence_id:
at_least_one = False
last_found = False
for one_date in meeting._get_recurrent_date_by_event():
for one_date in meeting.recurrence_id._get_occurrences(meeting.start):
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:
@@ -187,10 +188,10 @@ class AlarmManager(models.AbstractModel):
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:
if meeting.recurrency and meeting.recurrence_id:
b_found = False
last_found = False
for one_date in meeting._get_recurrent_date_by_event():
for one_date in meeting.recurrence_id._get_occurrences(meeting.start):
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:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,429 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from datetime import datetime, time
from dateutil import rrule
from dateutil.relativedelta import relativedelta, MO
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: 'mo',
rrule.TU.weekday: 'tu',
rrule.WE.weekday: 'we',
rrule.TH.weekday: 'th',
rrule.FR.weekday: 'fr',
rrule.SA.weekday: 'sa',
rrule.SU.weekday: 'su',
}
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 = [
('MO', 'Monday'),
('TU', 'Tuesday'),
('WE', 'Wednesday'),
('TH', 'Thursday'),
('FR', 'Friday'),
('SA', 'Saturday'),
('SU', 'Sunday'),
]
BYDAY_SELECTION = [
('1', 'First'),
('2', 'Second'),
('3', 'Third'),
('-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'
# TODO exdate?
base_event_id = fields.Many2one('calendar.event', ondelete='set null', copy=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)
mo = fields.Boolean()
tu = fields.Boolean()
we = fields.Boolean()
th = fields.Boolean()
fr = fields.Boolean()
sa = fields.Boolean()
su = 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)", "The day must be between 1 and 31"),
]
@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', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su', 'day', 'weekday')
def _compute_rrule(self):
for recurrence in self:
recurrence.rrule = recurrence._rrule_serialize()
def _inverse_rrule(self):
for recurrence in self:
if recurrence.rrule:
values = self._rrule_parse(recurrence.rrule, recurrence.dtstart)
recurrence.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 _apply_recurrence(self):
"""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']
for recurrence in self.filtered('base_event_id'):
self.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
ranges = set(recurrence._get_ranges(event.start, duration))
events_to_keep, ranges = recurrence._reconcile_events(ranges)
keep |= events_to_keep
[base_values] = event.copy_data()
event_vals += [dict(base_values, start=start, stop=stop, recurrence_id=recurrence.id) for start, stop in ranges]
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.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.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 = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
if 'Z' in rule_str 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_start_of_period(self, dt):
if self.rrule_type == 'daily':
return dt
elif self.rrule_type == 'weekly':
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'
week_start = rrule.weekday(week_start - 1) # expects an int from 0 to 6
return dt + relativedelta(weekday=week_start(-1))
elif self.rrule_type == 'monthly':
return dt + relativedelta(day=1)
elif self.rrule_type == 'yearly':
return dt
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 _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 US/Eastern (UTC−05:00)
# - at 6am US/Eastern = 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 US/Eastern timezone. US/Eastern is now UTC−04:00.
# From this point in time, 11am (UTC) is actually converted to 7am (US/Eastern) 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.mo,
rrule.TU.weekday: self.tu,
rrule.WE.weekday: self.we,
rrule.TH.weekday: self.th,
rrule.FR.weekday: self.fr,
rrule.SA.weekday: self.sa,
rrule.SU.weekday: self.su,
}.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, 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
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
)
-27
View File
@@ -1,27 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from odoo.addons.calendar.models.calendar_event import get_real_ids
class Attachment(models.Model):
_inherit = "ir.attachment"
@api.model
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
""" Convert the search on real ids in the case it was asked on virtual ids, then call super() """
args = list(args)
if any([leaf for leaf in args if leaf[0] == "res_model" and leaf[2] == 'calendar.event']):
for index in range(len(args)):
if args[index][0] == "res_id" and isinstance(args[index][2], str):
args[index] = (args[index][0], args[index][1], get_real_ids(args[index][2]))
return super(Attachment, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
def write(self, vals):
""" When posting an attachment (new or not), convert the virtual ids in real ids. """
if isinstance(vals.get('res_id'), str):
vals['res_id'] = get_real_ids(vals.get('res_id'))
return super(Attachment, self).write(vals)
+1 -2
View File
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo
from odoo import models, SUPERUSER_ID
from odoo import models
from odoo.http import request
from odoo.api import Environment
-31
View File
@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from odoo.addons.calendar.models.calendar_event import get_real_ids
class Message(models.Model):
_inherit = "mail.message"
@api.model
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
""" Convert the search on real ids in the case it was asked on virtual ids, then call super() """
args = list(args)
for index in range(len(args)):
if args[index][0] == "res_id":
if isinstance(args[index][2], str):
args[index] = (args[index][0], args[index][1], get_real_ids(args[index][2]))
elif isinstance(args[index][2], list):
args[index] = (args[index][0], args[index][1], [get_real_ids(x) for x in args[index][2]])
return super(Message, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
@api.model
def _find_allowed_model_wise(self, doc_model, doc_dict):
if doc_model == 'calendar.event':
order = self._context.get('order', self.env[doc_model]._order)
for virtual_id in self.env[doc_model].with_context(active_test=False).search([('id', 'in', list(doc_dict))], order=order).ids:
doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
return super(Message, self)._find_allowed_model_wise(doc_model, doc_dict)
+1 -3
View File
@@ -5,8 +5,6 @@ from datetime import datetime
from odoo import api, fields, models
from odoo.addons.calendar.models.calendar_event import get_real_ids
class Partner(models.Model):
_inherit = 'res.partner'
@@ -20,7 +18,7 @@ class Partner(models.Model):
datas = []
meeting = None
if meeting_id:
meeting = self.env['calendar.event'].browse(get_real_ids(meeting_id))
meeting = self.env['calendar.event'].browse(meeting_id)
for partner in self:
data = partner.name_get()[0]
@@ -10,3 +10,4 @@ access_calendar_event_type_manager,calendar.event.type.manager,model_calendar_ev
access_calendar_alarm_manager,access_calendar_alarm_manager,model_calendar_alarm_manager,base.group_user,1,1,1,1
access_calendar_contacts_all,access_calendar_contacts_all,model_calendar_contacts,base.group_user,1,1,1,1
access_calendar_contacts,access_calendar_contacts,model_calendar_contacts,base.group_system,1,1,1,1
access_calendar_recurrence,access_calendar_recurrence,model_calendar_recurrence,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
10 access_calendar_alarm_manager access_calendar_alarm_manager model_calendar_alarm_manager base.group_user 1 1 1 1
11 access_calendar_contacts_all access_calendar_contacts_all model_calendar_contacts base.group_user 1 1 1 1
12 access_calendar_contacts access_calendar_contacts model_calendar_contacts base.group_system 1 1 1 1
13 access_calendar_recurrence access_calendar_recurrence model_calendar_recurrence base.group_user 1 1 1 1
@@ -0,0 +1,65 @@
odoo.define('calendar.CalendarController', function (require) {
"use strict";
const Controller = require('web.CalendarController');
const Dialog = require('web.Dialog');
const { qweb, _t } = require('web.core');
const CalendarController = Controller.extend({
_askRecurrenceUpdatePolicy() {
return new Promise((resolve, reject) => {
new Dialog(this, {
title: _t('Edit Recurrent event'),
size: 'small',
$content: $(qweb.render('calendar.RecurrentEventUpdate')),
buttons: [{
text: _t('Confirm'),
classes: 'btn-primary',
close: true,
click: function () {
resolve(this.$('input:checked').val());
},
}],
}).open();
})
},
// TODO factorize duplicated code
/**
* @override
* @private
* @param {OdooEvent} event
*/
async _onDropRecord(event) {
const _super = this._super; // reference to this._super is lost after async call
if (event.data.record.recurrency) {
const recurrenceUpdate = await this._askRecurrenceUpdatePolicy();
event.data = _.extend({}, event.data, {
'recurrenceUpdate': recurrenceUpdate,
});
}
_super.apply(this, arguments);
},
/**
* @override
* @private
* @param {OdooEvent} event
*/
async _onUpdateRecord(event) {
const _super = this._super; // reference to this._super is lost after async call
if (event.data.record.recurrency) {
const recurrenceUpdate = await this._askRecurrenceUpdatePolicy();
event.data = _.extend({}, event.data, {
'recurrenceUpdate': recurrenceUpdate,
});
}
_super.apply(this, arguments);
},
});
return CalendarController;
});
@@ -0,0 +1,24 @@
odoo.define('calendar.CalendarModel', function (require) {
"use strict";
const Model = require('web.CalendarModel');
const CalendarModel = Model.extend({
/**
* @override
* Transform fullcalendar event object to odoo Data object
*/
calendarEventToRecord(event) {
const data = this._super(event);
return _.extend({}, data, {
'recurrence_update': event.recurrenceUpdate,
});
}
});
return CalendarModel;
});
@@ -3,6 +3,8 @@ odoo.define('calendar.CalendarView', function (require) {
var CalendarPopover = require('web.CalendarPopover');
var CalendarRenderer = require('web.CalendarRenderer');
var CalendarController = require('calendar.CalendarController');
var CalendarModel = require('calendar.CalendarModel');
var CalendarView = require('web.CalendarView');
const session = require('web.session');
var viewRegistry = require('web.view_registry');
@@ -107,6 +109,8 @@ var AttendeeCalendarRenderer = CalendarRenderer.extend({
var AttendeeCalendarView = CalendarView.extend({
config: _.extend({}, CalendarView.prototype.config, {
Renderer: AttendeeCalendarRenderer,
Controller: CalendarController,
Model: CalendarModel,
}),
});
@@ -6,6 +6,18 @@
</t>
</t>
<div t-name="calendar.RecurrentEventUpdate">
<div class="form-check o_radio_item">
<input name="recurrence-update" type="radio" class="form-check-input o_radio_input" checked="true" value="self_only" id="self_only"/>
<label class="form-check-label o_form_label" for="self_only">This event</label>
</div>
<div class="form-check o_radio_item">
<input name="recurrence-update" type="radio" class="form-check-input o_radio_input" value="future_events" id="future_events"/>
<label class="form-check-label o_form_label" for="future_events">This and following events</label>
</div>
</div>
<t t-extend="mail.systray.ActivityMenu.Previews">
<t t-jquery="div.o_preview_title" t-operation="after">
<div t-if="activity and activity.type == 'meeting'">
+1
View File
@@ -3,3 +3,4 @@
from . import test_calendar
from . import test_calendar_recurrent_event_case2
from . import test_event_recurrence
+9 -140
View File
@@ -41,131 +41,6 @@ class TestCalendar(SavepointCaseWithUserDemo):
"Sanity check"
)
def test_calender_event(self):
# Now I will set recurrence for this event to occur monday and friday of week
data = {
'fr': 1,
'mo': 1,
'interval': 1,
'rrule_type': 'weekly',
'end_type': 'end_date',
'final_date': '2011-05-31 00:00:00',
'recurrency': True
}
self.event_tech_presentation.write(data)
# In order to check that recurrent events are views successfully in calendar view, I will open calendar view of events|
self.CalendarEvent.fields_view_get(False, 'calendar')
# In order to check that recurrent events are views successfully in calendar view, I will search for one of the recurrent event and count the number of events
rec_events = self.CalendarEvent.with_context({'virtual_id': True}).search([
('start', '>=', '2011-04-30 16:00:00'), ('start', '<=', '2011-05-31 00:00:00')
])
self.assertEqual(len(rec_events), 9, 'Wrong number of events found')
# Now I move a virtual event, to see that a real event is well created and depending from the native recurrence
before = self.CalendarEvent.with_context({'virtual_id': False}).search([
('start', '>=', '2011-04-30 16:00:00'), ('start', '<=', '2011-05-31 00:00:00')
])
# We start by detach the event
newevent = rec_events[1].detach_recurring_event()
newevent.with_context({'virtual_id': True}).write({'name': 'New Name', 'recurrency': True})
after = self.CalendarEvent.with_context({'virtual_id': False}).search([
('start', '>=', '2011-04-30 16:00:00'), ('start', '<=', '2011-05-31 00:00:00')
])
self.assertEqual(len(after), len(before) + 1, 'Wrong number of events found, after to have moved a virtual event')
new_event = after - before
self.assertEqual(new_event[0].recurrent_id, before.id, 'Recurrent_id not correctly passed to the new event')
# Now I will test All day event
allday_event = self.CalendarEvent.create({
'allday': 1,
'privacy': 'confidential',
'start': '2011-04-30 00:00:00',
'stop': '2011-04-30 00:00:00',
'description': 'All day technical test',
'location': 'School',
'name': 'All day test event'
})
# In order to check reminder I will first create reminder
res_alarm_day_before_event_starts = self.env['calendar.alarm'].create({
'name': '1 Day before event starts',
'duration': 1,
'interval': 'days',
'alarm_type': 'notification'
})
# Now I will assign this reminder to all day event|
allday_event.write({'alarm_ids': [(6, 0, [res_alarm_day_before_event_starts.id])]})
# I create a recuring rule for my event
calendar_event_sprint_review = self.CalendarEvent.create({
'name': 'Begin of month meeting',
'start': datetime.combine(fields.Date.today(), time(12, 0)),
'stop': datetime.combine(fields.Date.today(), time(18, 0)),
'recurrency': True,
'rrule': 'FREQ=MONTHLY;INTERVAL=1;COUNT=12;BYDAY=1MO'
})
# I check that the attributes are set correctly
self.assertEqual(calendar_event_sprint_review.rrule_type, 'monthly', 'rrule_type should be mothly')
self.assertEqual(calendar_event_sprint_review.count, 12, 'rrule_type should be mothly')
self.assertEqual(calendar_event_sprint_review.month_by, 'day', 'rrule_type should be mothly')
self.assertEqual(calendar_event_sprint_review.byday, '1', 'rrule_type should be mothly')
self.assertEqual(calendar_event_sprint_review.week_list, 'MO', 'rrule_type should be mothly')
def test_validation_error(self):
"""
Ideally this should build the base event in such a way that calling
write() triggers detach_recurring_event, but I've no idea how that
actually works so just calling it directly for now
"""
m = self.CalendarEvent.create({
'name': "wheee",
'start': '2017-07-12 14:30:00',
'allday': False,
'rrule': u'FREQ=WEEKLY;BYDAY=WE;INTERVAL=1;COUNT=100',
'duration': 0.5,
'stop': '2017-07-12 15:00:00',
})
self.assertEqual(
(str(m.start_datetime), str(m.stop_datetime)),
(u'2017-07-12 14:30:00', u'2017-07-12 15:00:00'),
"Sanity check"
)
partner_1 = self.env['res.partner'].create({'name': 'A First Partner'})
partner_2 = self.env['res.partner'].create({'name': 'A Second Partner'})
values = {
'allday': False,
'name': u'wheee',
'attendee_ids': [
(0, 0, {'state': u'needsAction', 'partner_id': partner_1.id, 'email': u'bob@example.com'}),
(0, 0, {'state': u'needsAction', 'partner_id': partner_2.id, 'email': u'ed@example.com'}),
],
'recurrency': True,
'privacy': u'public',
'stop': '2017-07-10 16:00:00',
'alarm_ids': [(6, 0, [])],
'start': '2017-07-10 15:30:00',
'location': u"XXX",
'duration': 0.5,
'partner_ids': [(4, partner_1.id), (4, partner_2.id)],
'description': u"A thing"
}
records = m.detach_recurring_event(values)
self.assertEqual(
(str(m.start_datetime), str(m.stop_datetime)),
('2017-07-12 14:30:00', u'2017-07-12 15:00:00'),
)
self.assertEqual(
(str(records.start_datetime), str(records.stop_datetime)),
(u'2017-07-10 15:30:00', u'2017-07-10 16:00:00'),
)
def test_event_order(self):
""" check the ordering of events when searching """
def create_event(name, date):
@@ -303,15 +178,15 @@ class TestCalendar(SavepointCaseWithUserDemo):
'start': '2018-10-27 14:30:00',
'allday': False,
'rrule': u'FREQ=DAILY;INTERVAL=1;COUNT=4',
'duration': 2,
'recurrency': True,
'stop': '2018-10-27 16:30:00',
'event_tz': 'Europe/Brussels',
})
start_recurring_dates = m.with_context({'tz': 'Europe/Brussels'})._get_recurrent_date_by_event()
start_recurring_dates = m.recurrence_id.calendar_event_ids.sorted('start').mapped('start')
self.assertEqual(len(start_recurring_dates), 4)
for d in start_recurring_dates:
self.assertEqual(d.tzinfo, pytz.UTC)
if d.day < 28: # DST switch happens between 2018-10-27 and 2018-10-28
self.assertEqual(d.hour, 14)
else:
@@ -402,6 +277,7 @@ class TestCalendar(SavepointCaseWithUserDemo):
('recipient_ids', 'in', partner.id),
('subject', 'like', m.name),
])
print(mail.mapped('subject'))
self.assertEqual(len(mail), 1)
partners = [
@@ -414,7 +290,7 @@ class TestCalendar(SavepointCaseWithUserDemo):
'name': "mailTest1",
'allday': False,
'rrule': u'FREQ=DAILY;INTERVAL=1;COUNT=5',
'duration': 0.5,
'recurrency': True,
'partner_ids': partner_ids,
'start': fields.Datetime.to_string(now + timedelta(days=10)),
'stop': fields.Datetime.to_string(now + timedelta(days=15)),
@@ -430,17 +306,10 @@ class TestCalendar(SavepointCaseWithUserDemo):
self.env['res.partner'].create({'name':'testuser4','email': u'alain@example.com'}),
])
partner_ids = [(6, False, [p.id for p in partners]),]
m.write({'partner_ids': partner_ids})
m.write({
'partner_ids': partner_ids,
'recurrence_update': 'all_events',
})
# more email should be sent
_test_one_mail_per_attendee(self, m, partners)
# calculate virtualid to detach one event
virtid = str(m.id) + '-' + ''.join(re.split('[\D]', fields.Datetime.to_string(now + timedelta(days=12))))
# detaching a virtual event in the chain
self.env['calendar.event'].browse(virtid).detach_recurring_event(values={'active':False})
# since the detach actually create an event in the backend
# we check that no mail notifications are sent to the attendees
_test_one_mail_per_attendee(self, m, partners)
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
from odoo.addons.calendar.models.calendar_event import calendar_id2real_id
class TestRecurrentEvent(common.TransactionCase):
@@ -35,9 +34,9 @@ class TestRecurrentEvent(common.TransactionCase):
self.CalendarEvent.create({
'start': '2011-04-18 11:47:00',
'stop': '2011-04-18 12:47:00',
'day': 0.0,
'day': 1,
'duration': 1.0,
'final_date': '2011-04-30',
'until': '2011-04-30',
'end_type': 'end_date',
'fr': True,
'mo': True,
@@ -50,158 +49,7 @@ class TestRecurrentEvent(common.TransactionCase):
})
# I search for all the recurrent weekly meetings.
meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([
meetings_count = self.CalendarEvent.search_count([
('start', '>=', '2011-03-13'), ('stop', '<=', '2011-05-13')
])
self.assertEqual(meetings_count, 10, 'Recurrent weekly meetings are not created !')
def test_recurrent_meeting3(self):
#I want to schedule a meeting every month for Sprint review.
self.calendar_event_sprint_review = self.CalendarEvent.create({
'count': 12,
'start': '2011-04-01 12:01:00',
'stop': '2011-04-01 13:01:00',
'day': 1,
'duration': 1.0,
'name': 'Sprint Review',
'recurrency': True,
'rrule_type': 'monthly'
})
# I search for all the recurrent monthly meetings.
meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([
('start', '>=', '2011-03-01'), ('stop', '<=', '2012-05-13')
])
self.assertEqual(meetings_count, 12, 'Recurrent weekly meetings are not created !')
# I change name of my monthly Sprint Review meeting.
idval = '%d-%s' % (self.calendar_event_sprint_review.id, '20110901130100')
self.CalendarEvent.browse(idval).write({'name': 'Sprint Review for google modules'})
# I check whether all the records are edited or not.
meetings = self.CalendarEvent.with_context({'virtual_id': True}).search([
('start', '>=', '2011-03-01'), ('stop', '<=', '2012-05-13')
])
for meeting in meetings:
self.assertEqual(meeting.name, 'Sprint Review for google modules', 'Name not changed for id: %s' % meeting.id)
# I detach first occurrence to check it is not modified by changing recurrent event.
min(meetings, key=lambda m: m.start).detach_recurring_event()
# I change description of my weekly meeting Review code with programmer.
idval = '%d-%s' % (self.calendar_event_sprint_review.id, '20110425124700')
self.CalendarEvent.browse(idval).write({'description': 'Review code of the module: sync_google_calendar.'})
# I check that detached event has not been edited.
detached_meeting = self.CalendarEvent.search([('recurrent_id', '=', self.calendar_event_sprint_review.id)])
self.assertEqual(detached_meeting.description, False, 'Detached event description changed for id: %s' % meeting.id)
# I verify wether I find an event by date range when subsequent to a detached one.
last_meeting = max(meetings, key=lambda m: m.start)
meetings = self.CalendarEvent.with_context({'virtual_id': True}).search([
('start', '<=', str(last_meeting.stop)), ('stop', '>=', str(last_meeting.start))
])
self.assertEqual(meetings.id, last_meeting.id, 'Last event should be found searching it by date range')
# I update the description of two meetings, and check that both have been updated
self.calendar_event_sprint_review.write({'description': "Some description"})
self.assertEqual(self.calendar_event_sprint_review.description, "Some description", "Event %d has not been updated" % self.calendar_event_sprint_review.id)
def test_recurrent_meeting4(self):
# I create a weekly meeting till a particular end date.
self.CalendarEvent.create({
'start': '2017-01-22 11:47:00',
'stop': '2017-01-22 12:47:00',
'day': 0.0,
'duration': 1.0,
'final_date': '2017-06-30',
'end_type': 'end_date',
'fr': True,
'mo': True,
'th': True,
'tu': True,
'we': True,
'name': 'Review code with programmer',
'recurrency': True,
'rrule_type': 'weekly'
})
# I search for a recurrent weekly meetings that take place at a given date.
meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([
('start', '<=', '2017-01-24'), ('stop', '>=', '2017-01-24'), ('name', '=', 'Review code with programmer')
])
self.assertEqual(meetings_count, 1, 'Recurrent weekly meetings are not found using date filter !')
# I search for a recurrent weekly meetings that take place at a given date and time.
meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([
('start', '<=', '2017-01-24 11:55:00'), ('stop', '>=', '2017-01-24 11:55:00'), ('name', '=', 'Review code with programmer')
])
self.assertEqual(meetings_count, 1, 'Recurrent weekly meetings are not found using time filter !')
# I search using the filter 'start date is set'
meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([
('start', '!=', False), ('stop', '>=', '2017-06-30 11:55:00'), ('name', '=', 'Review code with programmer')
])
self.assertEqual(meetings_count, 1, "Last recurrent weekly meetings are not found using 'is set' filter !")
# I search for a recurrent weekly meetings that take place at a given date and time.
meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([
('start', '<=', '2017-01-24 11:55:00'), ('stop', '>=', '2017-01-24 15:55:00')
])
self.assertEqual(meetings_count, 0, 'Too late recurrent meetings are found using time filter !')
# I search using a start filter but no stop
meetings_count = self.CalendarEvent.with_context({'virtual_id': True}).search_count([
('start', '>=', '2017-06-30 08:00:00'), ('name', '=', 'Review code with programmer')
])
self.assertEqual(meetings_count, 1, "Last recurrent weekly meetings are not found without stop filter !")
def test_recurrent_meeting5(self):
# I create a recurrent event and I check if the virtual_id are correct
self.CalendarEvent.create({
'count': 5,
'start': '2012-04-13 11:00:00',
'stop': '2012-04-13 12:00:00',
'duration': 1.0,
'name': 'Test Meeting',
'recurrency': True,
'rrule_type': 'daily'
})
# I search for the first recurrent meeting
meeting = self.CalendarEvent.with_context({'virtual_id': True}).search([
('start', '=', '2012-04-13 11:00:00'), ('stop', '=', '2012-04-13 12:00:00')
])
virutal_dates = calendar_id2real_id(meeting.id, with_date=True)
# virtual_dates are used by the calendar view and I check if the start date for the first virtual event is correct.
self.assertEqual(virutal_dates[1], '2012-04-13 11:00:00', "The virtual event doesn't have the correct start date !")
# virtual_dates are used by the calendar view and I check if the stop date for the first virtual event is correct.
self.assertEqual(virutal_dates[2], '2012-04-13 12:00:00', "The virtual event doesn't have the correct stop date !")
def test_recurrent_meeting6(self):
ev = self.CalendarEvent.create({
'name': 'Rec1',
'start': '2018-06-28 11:00:00',
'stop': '2018-06-28 12:00:00',
'day': 0.0,
'duration': 1.0,
'count': ' 2',
'end_type': 'count',
'fr': True,
'recurrency': True,
'allday': False,
'rrule_type': 'weekly'
})
#event 2018-07-06 and 2018-06-29
meetings = self.CalendarEvent.with_context({'virtual_id': True}).search([
'|', '&', ('start', '>=', '2018-06-30 00:00:00'), ('start', '<=', '2018-06-30 23:59:59'), ('allday', '=', True)
])
base_ids = [calendar_id2real_id(meeting.id, with_date=False) for meeting in meetings]
self.assertNotIn(ev.id, base_ids, "Event does not match the domain")
meetings = self.CalendarEvent.with_context({'virtual_id': True}).search([
'|', '&', ('start', '>=', '2018-06-29 00:00:00'), ('start', '<=', '2018-06-29 23:59:59'), ('allday', '=', True)
])
base_ids = [calendar_id2real_id(meeting.id, with_date=False) for meeting in meetings]
self.assertIn(ev.id, base_ids, "Event does match the domain")
@@ -0,0 +1,642 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import UserError
import pytz
from datetime import datetime, date
from dateutil.relativedelta import relativedelta
from odoo.tests.common import SavepointCase
class TestRecurrentEvents(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
lang = cls.env['res.lang']._lang_get(cls.env.user.lang)
lang.week_start = '1' # Monday
def assertEventDates(self, events, dates):
events = events.sorted('start')
self.assertEqual(len(events), len(dates), "Wrong number of events in the recurrence")
self.assertTrue(all(events.mapped('active')), "All events should be active")
for event, dates in zip(events, dates):
start, stop = dates
self.assertEqual(event.start, start)
self.assertEqual(event.stop, stop)
class TestCreateRecurrentEvents(TestRecurrentEvents):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.event = cls.env['calendar.event'].create({
'name': 'Recurrent Event',
'start': datetime(2019, 10, 25, 8, 0),
'stop': datetime(2019, 10, 27, 18, 0),
'recurrency': True,
})
def test_weekly_count(self):
""" Every week, on Tuesdays, for 3 occurences """
detached_events = self.event._apply_recurrence_values({
'rrule_type': 'weekly',
'tu': True,
'interval': 1,
'count': 3,
'event_tz': 'UTC',
})
self.assertEqual(detached_events, self.event, "It should be detached from the recurrence")
self.assertFalse(self.event.recurrence_id, "It should be detached from the recurrence")
recurrence = self.env['calendar.recurrence'].search([('base_event_id', '=', self.event.id)])
events = recurrence.calendar_event_ids
self.assertEqual(len(events), 3, "It should have 3 events in the recurrence")
self.assertEventDates(events, [
(datetime(2019, 10, 22, 8, 0), datetime(2019, 10, 24, 18, 0)),
(datetime(2019, 10, 29, 8, 0), datetime(2019, 10, 31, 18, 0)),
(datetime(2019, 11, 5, 8, 0), datetime(2019, 11, 7, 18, 0)),
])
def test_weekly_interval_2(self):
self.event._apply_recurrence_values({
'interval': 2,
'rrule_type': 'weekly',
'tu': True,
'count': 2,
'event_tz': 'UTC',
})
recurrence = self.env['calendar.recurrence'].search([('base_event_id', '=', self.event.id)])
events = recurrence.calendar_event_ids
self.assertEventDates(events, [
(datetime(2019, 10, 22, 8, 0), datetime(2019, 10, 24, 18, 0)),
(datetime(2019, 11, 5, 8, 0), datetime(2019, 11, 7, 18, 0)),
])
def test_weekly_until(self):
self.event._apply_recurrence_values({
'rrule_type': 'weekly',
'tu': True,
'interval': 2,
'end_type': 'end_date',
'until': datetime(2019, 11, 15),
'event_tz': 'UTC',
})
recurrence = self.env['calendar.recurrence'].search([('base_event_id', '=', self.event.id)])
events = recurrence.calendar_event_ids
self.assertEqual(len(events), 2, "It should have 2 events in the recurrence")
self.assertEventDates(events, [
(datetime(2019, 10, 22, 8, 0), datetime(2019, 10, 24, 18, 0)),
(datetime(2019, 11, 5, 8, 0), datetime(2019, 11, 7, 18, 0)),
])
def test_monthly_count_by_date(self):
self.event._apply_recurrence_values({
'rrule_type': 'monthly',
'interval': 2,
'month_by': 'date',
'day': 15,
'end_type': 'count',
'count': 3,
'event_tz': 'UTC',
})
recurrence = self.env['calendar.recurrence'].search([('base_event_id', '=', self.event.id)])
events = recurrence.calendar_event_ids
self.assertEqual(len(events), 3, "It should have 3 events in the recurrence")
self.assertEventDates(events, [
(datetime(2019, 10, 15, 8, 0), datetime(2019, 10, 17, 18, 0)),
(datetime(2019, 12, 15, 8, 0), datetime(2019, 12, 17, 18, 0)),
(datetime(2020, 2, 15, 8, 0), datetime(2020, 2, 17, 18, 0)),
])
def test_monthly_count_by_date_31(self):
self.event._apply_recurrence_values({
'rrule_type': 'monthly',
'interval': 1,
'month_by': 'date',
'day': 31,
'end_type': 'count',
'count': 3,
'event_tz': 'UTC',
})
recurrence = self.env['calendar.recurrence'].search([('base_event_id', '=', self.event.id)])
events = recurrence.calendar_event_ids
self.assertEqual(len(events), 3, "It should have 3 events in the recurrence")
self.assertEventDates(events, [
(datetime(2019, 10, 31, 8, 0), datetime(2019, 11, 2, 18, 0)),
# Missing 31th in November
(datetime(2019, 12, 31, 8, 0), datetime(2020, 1, 2, 18, 0)),
(datetime(2020, 1, 31, 8, 0), datetime(2020, 2, 2, 18, 0)),
])
def test_monthly_until_by_day(self):
""" Every 2 months, on the third Tuesday, until 15th March 2020 """
self.event._apply_recurrence_values({
'rrule_type': 'monthly',
'interval': 2,
'month_by': 'day',
'byday': '3',
'weekday': 'TU',
'end_type': 'end_date',
'until': date(2020, 3, 15),
'event_tz': 'UTC',
})
recurrence = self.env['calendar.recurrence'].search([('base_event_id', '=', self.event.id)])
events = recurrence.calendar_event_ids
self.assertEqual(len(events), 3, "It should have 3 events in the recurrence")
self.assertEventDates(events, [
(datetime(2019, 10, 15, 8, 0), datetime(2019, 10, 17, 18, 0)),
(datetime(2019, 12, 17, 8, 0), datetime(2019, 12, 19, 18, 0)),
(datetime(2020, 2, 18, 8, 0), datetime(2020, 2, 20, 18, 0)),
])
def test_monthly_until_by_day_last(self):
""" Every 2 months, on the last Wednesday, until 15th January 2020 """
self.event._apply_recurrence_values({
'interval': 2,
'rrule_type': 'monthly',
'month_by': 'day',
'weekday': 'WE',
'byday': '-1',
'end_type': 'end_date',
'until': date(2020, 1, 15),
'event_tz': 'UTC',
})
recurrence = self.env['calendar.recurrence'].search([('base_event_id', '=', self.event.id)])
events = recurrence.calendar_event_ids
self.assertEqual(len(events), 2, "It should have 3 events in the recurrence")
self.assertEventDates(events, [
(datetime(2019, 10, 30, 8, 0), datetime(2019, 11, 1, 18, 0)),
(datetime(2019, 12, 25, 8, 0), datetime(2019, 12, 27, 18, 0)),
])
def test_yearly_count(self):
self.event._apply_recurrence_values({
'interval': 2,
'rrule_type': 'yearly',
'count': 2,
'event_tz': 'UTC',
})
events = self.event.recurrence_id.calendar_event_ids
self.assertEqual(len(events), 2, "It should have 3 events in the recurrence")
self.assertEventDates(events, [
(self.event.start, self.event.stop),
(self.event.start + relativedelta(years=2), self.event.stop + relativedelta(years=2)),
])
def test_dst_timezone(self):
""" Test hours stays the same, regardless of DST changes """
self.event.start = datetime(2002, 10, 26, 10, 0)
self.event.stop = datetime(2002, 10, 26, 12, 0)
self.event._apply_recurrence_values({
'interval': 2,
'rrule_type': 'weekly',
'mo': True,
'count': '2',
'event_tz': 'US/Eastern', # DST change on 2002/10/27
})
recurrence = self.env['calendar.recurrence'].search([('base_event_id', '=', self.event.id)])
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2002, 10, 21, 10, 0), datetime(2002, 10, 21, 12, 0)),
(datetime(2002, 11, 4, 11, 0), datetime(2002, 11, 4, 13, 0)),
])
def test_ambiguous_dst_time_winter(self):
""" Test hours stays the same, regardless of DST changes """
eastern = pytz.timezone('US/Eastern')
dt = eastern.localize(datetime(2002, 10, 20, 1, 30, 00)).astimezone(pytz.utc).replace(tzinfo=None)
# Next occurence happens at 1:30am on 27th Oct 2002 which happened twice in the US/Eastern
# timezone when the clocks where put back at the end of Daylight Saving Time
self.event.start = dt
self.event.stop = dt + relativedelta(hours=1)
self.event._apply_recurrence_values({
'interval': 1,
'rrule_type': 'weekly',
'su': True,
'count': '2',
'event_tz': 'US/Eastern' # DST change on 2002/4/7
})
events = self.event.recurrence_id.calendar_event_ids
self.assertEqual(events.mapped('duration'), [1, 1])
self.assertEventDates(events, [
(datetime(2002, 10, 20, 5, 30), datetime(2002, 10, 20, 6, 30)),
(datetime(2002, 10, 27, 6, 30), datetime(2002, 10, 27, 7, 30)),
])
def test_ambiguous_dst_time_spring(self):
""" Test hours stays the same, regardless of DST changes """
eastern = pytz.timezone('US/Eastern')
dt = eastern.localize(datetime(2002, 3, 31, 2, 30, 00)).astimezone(pytz.utc).replace(tzinfo=None)
# Next occurence happens 2:30am on 7th April 2002 which never happened at all in the
# US/Eastern timezone, as the clocks where put forward at 2:00am skipping the entire hour
self.event.start = dt
self.event.stop = dt + relativedelta(hours=1)
self.event._apply_recurrence_values({
'interval': 1,
'rrule_type': 'weekly',
'su': True,
'count': '2',
'event_tz': 'US/Eastern' # DST change on 2002/4/7
})
events = self.event.recurrence_id.calendar_event_ids
self.assertEqual(events.mapped('duration'), [1, 1])
# The event begins at "the same time" (i.e. 2h30 after midnight), but that day, 2h30 after midnight happens to be at 3:30 am
self.assertEventDates(events, [
(datetime(2002, 3, 31, 7, 30), datetime(2002, 3, 31, 8, 30)),
(datetime(2002, 4, 7, 7, 30), datetime(2002, 4, 7, 8, 30)),
])
def test_ambiguous_full_day(self):
""" Test date stays the same, regardless of DST changes """
self.event.start = datetime(2020, 3, 23, 0, 0)
self.event.stop = datetime(2020, 3, 23, 23, 59)
self.event.allday = True
self.event._apply_recurrence_values({
'interval': 1,
'rrule_type': 'weekly',
'mo': True,
'count': 2,
'event_tz': 'Europe/Brussels' # DST change on 2020/3/23
})
events = self.event.recurrence_id.calendar_event_ids
self.assertEventDates(events, [
(datetime(2020, 3, 23, 0, 0), datetime(2020, 3, 23, 23, 59)),
(datetime(2020, 3, 30, 0, 0), datetime(2020, 3, 30, 23, 59)),
])
class TestUpdateRecurrentEvents(TestRecurrentEvents):
@classmethod
def setUpClass(cls):
super().setUpClass()
event = cls.env['calendar.event'].create({
'name': 'Recurrent Event',
'start': datetime(2019, 10, 22, 1, 0),
'stop': datetime(2019, 10, 24, 18, 0),
'recurrency': True,
'rrule_type': 'weekly',
'tu': True,
'interval': 1,
'count': 3,
'event_tz': 'Etc/GMT-4',
})
cls.recurrence = event.recurrence_id
cls.events = event.recurrence_id.calendar_event_ids.sorted('start')
def test_shift_future(self):
event = self.events[1]
self.events[1].write({
'recurrence_update': 'future_events',
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
self.assertEqual(self.recurrence.end_type, 'end_date')
self.assertEqual(self.recurrence.until, date(2019, 10, 27))
self.assertEventDates(self.recurrence.calendar_event_ids, [
(datetime(2019, 10, 22, 1, 0), datetime(2019, 10, 24, 18, 0)),
])
new_recurrence = event.recurrence_id
self.assertNotEqual(self.recurrence, new_recurrence)
self.assertEqual(new_recurrence.count, 2)
self.assertEqual(new_recurrence.dtstart, datetime(2019, 11, 2, 1, 0))
self.assertFalse(new_recurrence.tu)
self.assertTrue(new_recurrence.sa)
self.assertEventDates(new_recurrence.calendar_event_ids, [
(datetime(2019, 11, 2, 1, 0), datetime(2019, 11, 5, 18, 0)),
(datetime(2019, 11, 9, 1, 0), datetime(2019, 11, 12, 18, 0)),
])
def test_shift_future_first(self):
event = self.events[0]
self.events[0].write({
'recurrence_update': 'future_events',
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
new_recurrence = event.recurrence_id
self.assertFalse(self.recurrence.exists())
self.assertEqual(new_recurrence.count, 3)
self.assertEqual(new_recurrence.dtstart, datetime(2019, 10, 26, 1, 0))
self.assertFalse(new_recurrence.tu)
self.assertTrue(new_recurrence.sa)
self.assertEventDates(new_recurrence.calendar_event_ids, [
(datetime(2019, 10, 26, 1, 0), datetime(2019, 10, 29, 18, 0)),
(datetime(2019, 11, 2, 1, 0), datetime(2019, 11, 5, 18, 0)),
(datetime(2019, 11, 9, 1, 0), datetime(2019, 11, 12, 18, 0)),
])
def test_shift_reapply(self):
event = self.events[2]
self.events[2].write({
'recurrence_update': 'future_events',
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
# re-Applying the first recurrence should be idempotent
self.recurrence._apply_recurrence()
self.assertEventDates(self.recurrence.calendar_event_ids, [
(datetime(2019, 10, 22, 1, 0), datetime(2019, 10, 24, 18, 0)),
(datetime(2019, 10, 29, 1, 0), datetime(2019, 10, 31, 18, 0)),
])
def test_shift_all(self):
event = self.events[1]
with self.assertRaises(UserError):
event.write({
'recurrence_update': 'all_events',
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
def test_change_week_day_rrule(self):
recurrence = self.events.recurrence_id
recurrence.rrule = 'FREQ=WEEKLY;COUNT=3;BYDAY=WE' # from TU to WE
self.assertFalse(self.recurrence.tu)
self.assertTrue(self.recurrence.we)
def test_shift_all_base_inactive(self):
self.recurrence.base_event_id.active = False
event = self.events[1]
with self.assertRaises(UserError):
event.write({
'recurrence_update': 'all_events',
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
def test_shift_all_with_outlier(self):
outlier = self.events[1]
outlier.write({
'recurrence_update': 'self_only',
'start': datetime(2019, 9, 26, 1, 0), # Thursday
'stop': datetime(2019, 9, 26, 18, 0),
})
event = self.events[0]
with self.assertRaises(UserError):
event.write({
'recurrence_update': 'all_events',
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
def test_update_recurrence_future(self):
event = self.events[1]
event.write({
'recurrence_update': 'future_events',
'fr': True, # recurrence is now Tuesday AND Friday
'count': 4,
})
self.assertEventDates(self.recurrence.calendar_event_ids, [
(datetime(2019, 10, 22, 1, 0), datetime(2019, 10, 24, 18, 0)), # Tu
])
self.assertEventDates(event.recurrence_id.calendar_event_ids, [
(datetime(2019, 10, 29, 1, 0), datetime(2019, 10, 31, 18, 0)), # Tu
(datetime(2019, 11, 1, 1, 0), datetime(2019, 11, 3, 18, 0)), # Fr
(datetime(2019, 11, 5, 1, 0), datetime(2019, 11, 7, 18, 0)), # Tu
(datetime(2019, 11, 8, 1, 0), datetime(2019, 11, 10, 18, 0)), # Fr
])
events = event.recurrence_id.calendar_event_ids.sorted('start')
self.assertEqual(events[0], self.events[1], "Events on Tuesdays should not have changed")
self.assertEqual(events[2], self.events[2], "Events on Tuesdays should not have changed")
self.assertNotEqual(events.recurrence_id, self.recurrence, "Events should no longer be linked to the original recurrence")
self.assertEqual(events.recurrence_id.count, 4, "The new recurrence should have 4")
self.assertTrue(event.recurrence_id.tu)
self.assertTrue(event.recurrence_id.fr)
def test_update_recurrence_all(self):
with self.assertRaises(UserError):
self.events[1].write({
'recurrence_update': 'all_events',
'mo': True, # recurrence is now Tuesday AND Monday
})
def test_shift_single(self):
event = self.events[1]
event.write({
'recurrence_update': 'self_only',
'name': "Updated event",
'start': event.start - relativedelta(hours=2)
})
self.events[0].write({
'recurrence_update': 'future_events',
'start': event.start + relativedelta(hours=4),
'stop': event.stop + relativedelta(hours=5),
})
def test_break_recurrence_future(self):
event = self.events[1]
event.write({
'recurrence_update': 'future_events',
'recurrency': False,
})
self.assertFalse(event.recurrence_id)
self.assertTrue(self.events[0].active)
self.assertTrue(self.events[1].active)
self.assertFalse(self.events[2].exists())
self.assertEqual(self.recurrence.until, date(2019, 10, 27))
self.assertEqual(self.recurrence.end_type, 'end_date')
self.assertEventDates(self.recurrence.calendar_event_ids, [
(datetime(2019, 10, 22, 1, 0), datetime(2019, 10, 24, 18, 0)),
])
def test_break_recurrence_all(self):
event = self.events[1]
event.write({
'recurrence_update': 'all_events',
'recurrency': False,
'count': 0, # In practice, JS framework sends updated recurrency fields, since they have been recomputed, triggered by the `recurrency` change
})
self.assertFalse(self.events[0].exists())
self.assertTrue(event.active)
self.assertFalse(self.events[2].exists())
self.assertFalse(event.recurrence_id)
self.assertFalse(self.recurrence.exists())
def test_all_day_shift(self):
recurrence = self.env['calendar.event'].create({
'name': 'Recurrent Event',
'start_date': datetime(2019, 10, 22),
'stop_date': datetime(2019, 10, 24),
'recurrency': True,
'rrule_type': 'weekly',
'tu': True,
'interval': 1,
'count': 3,
'event_tz': 'Etc/GMT-4',
'allday': True,
}).recurrence_id
events = recurrence.calendar_event_ids.sorted('start')
event = events[1]
event.write({
'recurrence_update': 'future_events',
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
self.assertEqual(recurrence.end_type, 'end_date')
self.assertEqual(recurrence.until, date(2019, 10, 27))
self.assertEventDates(recurrence.calendar_event_ids, [
(datetime(2019, 10, 22, 8, 0), datetime(2019, 10, 24, 18, 0)),
])
new_recurrence = event.recurrence_id
self.assertNotEqual(recurrence, new_recurrence)
self.assertEqual(new_recurrence.count, 2)
self.assertEqual(new_recurrence.dtstart, datetime(2019, 11, 2, 8, 0))
self.assertFalse(new_recurrence.tu)
self.assertTrue(new_recurrence.sa)
self.assertEventDates(new_recurrence.calendar_event_ids, [
(datetime(2019, 11, 2, 8, 0), datetime(2019, 11, 5, 18, 0)),
(datetime(2019, 11, 9, 8, 0), datetime(2019, 11, 12, 18, 0)),
])
# TODO test followers, and alarms are copied
class TestUpdateMultiDayWeeklyRecurrentEvents(TestRecurrentEvents):
@classmethod
def setUpClass(cls):
super().setUpClass()
event = cls.env['calendar.event'].create({
'name': 'Recurrent Event',
'start': datetime(2019, 10, 22, 1, 0),
'stop': datetime(2019, 10, 24, 18, 0),
'recurrency': True,
'rrule_type': 'weekly',
'tu': True,
'fr': True,
'interval': 1,
'count': 3,
'event_tz': 'Etc/GMT-4',
})
cls.recurrence = event.recurrence_id
cls.events = event.recurrence_id.calendar_event_ids.sorted('start')
# Tuesday datetime(2019, 10, 22, 1, 0)
# Friday datetime(2019, 10, 25, 1, 0)
# Tuesday datetime(2019, 10, 29, 1, 0)
def test_shift_all_multiple_weekdays(self):
event = self.events[0] # Tuesday
with self.assertRaises(UserError):
event.write({
'recurrence_update': 'all_events',
'start': event.start + relativedelta(days=2),
'stop': event.stop + relativedelta(days=2),
})
def test_shift_all_multiple_weekdays_duration(self):
event = self.events[0] # Tuesday
with self.assertRaises(UserError):
event.write({
'recurrence_update': 'all_events',
'start': event.start + relativedelta(days=2),
'stop': event.stop + relativedelta(days=3),
})
def test_shift_future_multiple_weekdays(self):
event = self.events[1] # Friday
event.write({
'recurrence_update': 'future_events',
'start': event.start + relativedelta(days=3),
'stop': event.stop + relativedelta(days=3),
})
self.assertTrue(self.recurrence.fr)
self.assertTrue(self.recurrence.tu)
self.assertTrue(event.recurrence_id.tu)
self.assertTrue(event.recurrence_id.mo)
self.assertFalse(event.recurrence_id.fr)
self.assertEqual(event.recurrence_id.count, 2)
class TestUpdateMonthlyByDay(TestRecurrentEvents):
@classmethod
def setUpClass(cls):
super().setUpClass()
event = cls.env['calendar.event'].create({
'name': 'Recurrent Event',
'start': datetime(2019, 10, 15, 1, 0),
'stop': datetime(2019, 10, 16, 18, 0),
'recurrency': True,
'rrule_type': 'monthly',
'interval': 1,
'count': 3,
'month_by': 'day',
'weekday': 'TU',
'byday': '3',
'event_tz': 'Etc/GMT-4',
})
cls.recurrence = event.recurrence_id
cls.events = event.recurrence_id.calendar_event_ids.sorted('start')
# datetime(2019, 10, 15, 1, 0)
# datetime(2019, 11, 19, 1, 0)
# datetime(2019, 12, 17, 1, 0)
def test_shift_all(self):
event = self.events[1]
with self.assertRaises(UserError):
event.write({
'recurrence_update': 'all_events',
'start': event.start - relativedelta(days=5),
'stop': event.stop - relativedelta(days=4),
})
# LUL TODO test shift future
class TestUpdateMonthlyByDate(TestRecurrentEvents):
@classmethod
def setUpClass(cls):
super().setUpClass()
event = cls.env['calendar.event'].create({
'name': 'Recurrent Event',
'start': datetime(2019, 10, 22, 1, 0),
'stop': datetime(2019, 10, 24, 18, 0),
'recurrency': True,
'rrule_type': 'monthly',
'interval': 1,
'count': 3,
'month_by': 'date',
'day': 22,
'event_tz': 'Etc/GMT-4',
})
cls.recurrence = event.recurrence_id
cls.events = event.recurrence_id.calendar_event_ids.sorted('start')
# datetime(2019, 10, 22, 1, 0)
# datetime(2019, 11, 22, 1, 0)
# datetime(2019, 12, 22, 1, 0)
def test_shift_future(self):
event = self.events[1]
event.write({
'recurrence_update': 'future_events',
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
self.assertEventDates(self.recurrence.calendar_event_ids, [
(datetime(2019, 10, 22, 1, 0), datetime(2019, 10, 24, 18, 0)),
])
self.assertEventDates(event.recurrence_id.calendar_event_ids, [
(datetime(2019, 11, 26, 1, 0), datetime(2019, 11, 29, 18, 0)),
(datetime(2019, 12, 26, 1, 0), datetime(2019, 12, 29, 18, 0)),
])
def test_shift_all(self):
event = self.events[1]
with self.assertRaises(UserError):
event.write({
'recurrence_update': 'all_events',
'start': event.start + relativedelta(days=4),
'stop': event.stop + relativedelta(days=5),
})
def test_update_all(self):
event = self.events[1]
with self.assertRaises(UserError):
event.write({
'recurrence_update': 'all_events',
'day': 25,
})
@@ -4,6 +4,8 @@
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/calendar/static/src/scss/calendar.scss"/>
<script type="text/javascript" src="/calendar/static/src/js/base_calendar.js"></script>
<script type="text/javascript" src="/calendar/static/src/js/calendar_controller.js"></script>
<script type="text/javascript" src="/calendar/static/src/js/calendar_model.js"></script>
<script type="text/javascript" src="/calendar/static/src/js/calendar_view.js"></script>
<script type="text/javascript" src="/calendar/static/src/js/mail_activity.js"></script>
<script type="text/javascript" src="/calendar/static/src/js/systray_activity_menu.js"></script>
+14 -11
View File
@@ -85,6 +85,10 @@
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<form string="Meetings">
<div attrs="{'invisible': [('recurrence_id','=',False)]}" class="alert alert-info oe_edit_only" role="status">
<p>Edit recurring event</p>
<field name="recurrence_update" widget="radio"/>
</div>
<sheet>
<div class="oe_button_box" name="button_box">
<button string="Document" icon="fa-bars" type="object" name="action_open_calendar_event" attrs="{'invisible': ['|', ('res_model', '=', False), ('res_id', '=', False)]}"/>
@@ -113,21 +117,16 @@
</div>
<notebook>
<page name="page_details" string="Meeting Details">
<group attrs="{'invisible': [('recurrency','==',False)]}" class="oe_edit_only ">
<p class='alert alert-warning' role="status"> This event is linked to a recurrence...<br/>
<button type="object" name="action_detach_recurring_event" string="Update only this instance" help="Click here to update only this instance and not all recurrences. " class="oe_link"/>
</p>
</group>
<group>
<group>
<field name="start" attrs="{'invisible': True}"/>
<field name="stop" attrs="{'invisible': True}"/>
<field name="id" attrs="{'invisible': True}"/>
<field name="start_date" string="Starting at" attrs="{'required': [('allday','=',True)], 'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/>
<field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/>
<field name="start_date" string="Starting at" attrs="{'required': [('allday','=',True)], 'invisible': [('allday','=',False)]}" force_save="1"/>
<field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)]}" force_save="1"/>
<field name="start_datetime" string="Starting at" attrs="{'required': [('allday','=',False)], 'invisible': [('allday','=',True)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<field name="start_datetime" string="Starting at" attrs="{'required': [('allday','=',False)], 'invisible': [('allday','=',True)]}"/>
<field name="stop_datetime" invisible="1"/>
<label for="duration" attrs="{'invisible': [('allday','=',True)]}"/>
<div attrs="{'invisible': [('allday','=',True)]}">
@@ -165,7 +164,7 @@
<div class="o_row">
<field name="end_type" attrs="{'required': [('recurrency', '=', True)]}"/>
<field name="count" attrs="{'invisible': [('end_type', '!=', 'count')], 'required': [('recurrency', '=', True)]}"/>
<field name="final_date" attrs="{'invisible': [('end_type', '!=', 'end_date')], 'required': [('end_type', '=', 'end_date'), ('recurrency', '=', True)]}"/>
<field name="until" attrs="{'invisible': [('end_type', '!=', 'end_date')], 'required': [('end_type', '=', 'end_date'), ('recurrency', '=', True)]}"/>
</div>
</group>
<group attrs="{'invisible': [('rrule_type', '!=', 'weekly')]}" name="weekdays">
@@ -187,7 +186,7 @@
<field name="byday" string="The"
attrs="{'required': [('recurrency', '=', True), ('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
'invisible': [('month_by', '!=', 'day')]}"/>
<field name="week_list" nolabel="1"
<field name="weekday" nolabel="1"
attrs="{'required': [('recurrency', '=', True), ('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
'invisible': [('month_by', '!=', 'day')]}"/>
</div>
@@ -197,7 +196,7 @@
<group>
<field name="privacy"/>
<field name="show_as"/>
<field name="recurrent_id" invisible="1" />
<field name="recurrence_id" invisible="1" />
</group>
</group>
</page>
@@ -268,6 +267,9 @@
<field name="is_highlighted" invisible="1"/>
<field name="description"/>
<field name="privacy"/>
<!-- For recurrence update Dialog -->
<field name="recurrency" invisible="1"/>
<field name="recurrence_update" invisible="1"/>
</calendar>
</field>
</record>
@@ -283,6 +285,7 @@
<field name="user_id"/>
<field name="show_as"/>
<field name="privacy" string="Privacy"/>
<!-- TODO clean this filter and context key -->
<filter string="My Meetings" help="My Meetings" name="mymeetings" context='{"mymeetings": 1}'/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mail_invite
-22
View File
@@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from odoo.addons.calendar.models.calendar_event import get_real_ids
class MailInvite(models.TransientModel):
_inherit = 'mail.wizard.invite'
@api.model
def default_get(self, fields):
""" In case someone clicked on 'invite others' wizard in the followers widget, transform virtual ids in real ids """
if 'default_res_id' in self._context:
self = self.with_context(default_res_id=get_real_ids(self._context['default_res_id']))
result = super(MailInvite, self).default_get(fields)
if 'res_id' in result:
result['res_id'] = get_real_ids(result['res_id'])
return result
+1 -1
View File
@@ -64,7 +64,7 @@ class AlarmManager(models.AbstractModel):
if event.recurrency:
found = False
for event_start in event._get_recurrent_date_by_event():
for event_start in event.recurrence_id._get_occurrences(event.start):
event_start = event_start.replace(tzinfo=None)
last_found = self.do_check_alarm_for_one_date(event_start, event, max_delta, 0, 'sms', after=last_sms_cron, missing=True)
for alert in last_found:
+2 -2
View File
@@ -13,9 +13,9 @@ class Meeting(models.Model):
@api.model
def get_fields_need_update_google(self):
recurrent_fields = self._get_recurrent_fields()
return recurrent_fields + ['name', 'description', 'allday', 'start', 'date_end', 'stop',
return recurrent_fields | {'name', 'description', 'allday', 'start', 'date_end', 'stop',
'attendee_ids', 'alarm_ids', 'location', 'privacy', 'active',
'start_date', 'start_datetime', 'stop_date', 'stop_datetime']
'start_date', 'start_datetime', 'stop_date', 'stop_datetime'}
def write(self, values):
sync_fields = set(self.get_fields_need_update_google())
@@ -17,6 +17,7 @@ var fieldsToGather = [
"date_delay",
"date_stop",
"all_day",
"recurrence_update"
];
var CalendarView = AbstractView.extend({