[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:
@@ -3,4 +3,3 @@
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
@@ -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'">
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
from . import test_calendar
|
||||
from . import test_calendar_recurrent_event_case2
|
||||
from . import test_event_recurrence
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)]"/>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mail_invite
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user