Files
odoo_source/addons/google_calendar/models/calendar_recurrence_rule.py
T
Arnaud Joset a67dc7e151 [FIX] google_calendar: chose internal users when portal and internal users share the same email
Before this commit, when two users shared the same mail, they were ordered by alphabetical order.
This commit makes sure that the internal user is preferred.

closes odoo/odoo#84805

X-original-commit: 992b7a00f8eb567aed730420d0d10fa476fb9dd7
Signed-off-by: Thibault Francois <tfr@odoo.com>
Signed-off-by: Arnaud Joset <arj@odoo.com>
2022-02-17 16:41:56 +00:00

231 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
import logging
from odoo import api, models, Command
from odoo.tools import email_normalize
from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService
_logger = logging.getLogger(__name__)
class RecurrenceRule(models.Model):
_name = 'calendar.recurrence'
_inherit = ['calendar.recurrence', 'google.calendar.sync']
def _apply_recurrence(self, specific_values_creation=None, no_send_edit=False, generic_values_creation=None):
events = self.filtered('need_sync').calendar_event_ids
detached_events = super()._apply_recurrence(specific_values_creation, no_send_edit,
generic_values_creation)
google_service = GoogleCalendarService(self.env['google.service'])
# If a synced event becomes a recurrence, the event needs to be deleted from
# Google since it's now the recurrence which is synced.
# Those events are kept in the database and their google_id is updated
# according to the recurrence google_id, therefore we need to keep an inactive copy
# of those events with the original google id. The next sync will then correctly
# delete those events from Google.
vals = []
for event in events.filtered('google_id'):
if event.active and event.google_id != event.recurrence_id._get_event_google_id(event):
vals += [{
'name': event.name,
'google_id': event.google_id,
'start': event.start,
'stop': event.stop,
'active': False,
'need_sync': True,
}]
event._google_delete(google_service, event.google_id)
event.google_id = False
self.env['calendar.event'].create(vals)
self.calendar_event_ids.need_sync = False
return detached_events
def _get_event_google_id(self, event):
"""Return the Google id of recurring event.
Google ids of recurrence instances are formatted as: {recurrence google_id}_{UTC starting time in compacted ISO8601}
"""
if self.google_id:
if event.allday:
time_id = event.start_date.isoformat().replace('-', '')
else:
# '-' and ':' are optional in ISO8601
start_compacted_iso8601 = event.start.isoformat().replace('-', '').replace(':', '')
# Z at the end for UTC
time_id = '%sZ' % start_compacted_iso8601
return '%s_%s' % (self.google_id, time_id)
return False
def _write_events(self, values, dtstart=None):
values.pop('google_id', False)
# If only some events are updated, sync those events.
values['need_sync'] = bool(dtstart)
return super()._write_events(values, dtstart=dtstart)
def _cancel(self):
self.calendar_event_ids._cancel()
super()._cancel()
def _get_google_synced_fields(self):
return {'rrule'}
@api.model
def _restart_google_sync(self):
self.env['calendar.recurrence'].search(self._get_sync_domain()).write({
'need_sync': True,
})
def _write_from_google(self, gevent, vals):
current_rrule = self.rrule
# event_tz is written on event in Google but on recurrence in Odoo
vals['event_tz'] = gevent.start.get('timeZone')
super()._write_from_google(gevent, vals)
base_event_time_fields = ['start', 'stop', 'allday']
new_event_values = self.env["calendar.event"]._odoo_values(gevent)
# We update the attendee status for all events in the recurrence
google_attendees = gevent.attendees or []
emails = [a.get('email') for a in google_attendees]
partners = self._get_sync_partner(emails)
existing_attendees = self.calendar_event_ids.attendee_ids
for attendee in zip(emails, partners, google_attendees):
email = attendee[0]
if email in existing_attendees.mapped('email'):
# Update existing attendees
existing_attendees.filtered(lambda att: att.email == email).write({'state': attendee[2].get('responseStatus')})
else:
# Create new attendees
if attendee[2].get('self'):
partner = self.env.user.partner_id
else:
partner = attendee[1]
self.calendar_event_ids.write({'attendee_ids': [(0, 0, {'state': attendee[2].get('responseStatus'), 'partner_id': partner.id})]})
if attendee[2].get('displayName') and not partner.name:
partner.name = attendee[2].get('displayName')
for odoo_attendee_email in set(existing_attendees.mapped('email')):
# Remove old attendees. Sometimes, several partners have the same email.
if email_normalize(odoo_attendee_email) not in emails:
attendees = existing_attendees.exists().filtered(lambda att: att.email == email_normalize(odoo_attendee_email))
self.calendar_event_ids.write({'need_sync': False, 'partner_ids': [Command.unlink(att.partner_id.id) for att in attendees]})
# Update the recurrence values
old_event_values = self.base_event_id and self.base_event_id.read(base_event_time_fields)[0]
if old_event_values and any(new_event_values[key] != old_event_values[key] for key in base_event_time_fields):
# we need to recreate the recurrence, time_fields were modified.
base_event_id = self.base_event_id
non_equal_values = [
(key, old_event_values[key] and old_event_values[key].strftime('%m/%d/%Y, %H:%M:%S'), '-->',
new_event_values[key] and new_event_values[key].strftime('%m/%d/%Y, %H:%M:%S')
) for key in ['start', 'stop'] if new_event_values[key] != old_event_values[key]
]
log_msg = f"Recurrence {self.id} {self.rrule} has all events ({len(self.calendar_event_ids.ids)}) deleted because of base event value change: {non_equal_values}"
_logger.info(log_msg)
# We archive the old events to recompute the recurrence. These events are already deleted on Google side.
# We can't call _cancel because events without user_id would not be deleted
(self.calendar_event_ids - base_event_id).google_id = False
(self.calendar_event_ids - base_event_id).unlink()
base_event_id.with_context(dont_notify=True).write(dict(new_event_values, google_id=False, need_sync=False))
if self.rrule == current_rrule:
# if the rrule has changed, it will be recalculated below
# There is no detached event now
self.with_context(dont_notify=True)._apply_recurrence()
else:
time_fields = (
self.env["calendar.event"]._get_time_fields()
| self.env["calendar.event"]._get_recurrent_fields()
)
# We avoid to write time_fields because they are not shared between events.
self._write_events(dict({
field: value
for field, value in new_event_values.items()
if field not in time_fields
}, need_sync=False)
)
# We apply the rrule check after the time_field check because the google_id are generated according
# to base_event start datetime.
if self.rrule != current_rrule:
detached_events = self._apply_recurrence()
detached_events.google_id = False
log_msg = f"Recurrence #{self.id} | current rule: {current_rrule} | new rule: {self.rrule} | remaining: {len(self.calendar_event_ids)} | removed: {len(detached_events)}"
_logger.info(log_msg)
detached_events.unlink()
def _create_from_google(self, gevents, vals_list):
attendee_values = {}
for gevent, vals in zip(gevents, vals_list):
base_values = dict(
self.env['calendar.event']._odoo_values(gevent), # FIXME default reminders
need_sync=False,
)
# If we convert a single event into a recurrency on Google, we should reuse this event on Odoo
# Google reuse the event google_id to identify the recurrence in that case
base_event = self.env['calendar.event'].search([('google_id', '=', vals['google_id'])])
if not base_event:
base_event = self.env['calendar.event'].create(base_values)
else:
# We override the base_event values because they could have been changed in Google interface
# The event google_id will be recalculated once the recurrence is created
base_event.write(dict(base_values, google_id=False))
vals['base_event_id'] = base_event.id
vals['calendar_event_ids'] = [(4, base_event.id)]
# event_tz is written on event in Google but on recurrence in Odoo
vals['event_tz'] = gevent.start.get('timeZone')
attendee_values[base_event.id] = {'attendee_ids': base_values.get('attendee_ids')}
recurrence = super(RecurrenceRule, self.with_context(dont_notify=True))._create_from_google(gevents, vals_list)
generic_values_creation = {
rec.id: attendee_values[rec.base_event_id.id]
for rec in recurrence if attendee_values.get(rec.base_event_id.id)
}
recurrence.with_context(dont_notify=True)._apply_recurrence(generic_values_creation=generic_values_creation)
return recurrence
def _get_sync_domain(self):
# Empty rrule may exists in historical data. It is not a desired behavior but it could have been created with
# older versions of the module. When synced, these recurrency may come back from Google after database cleaning
# and trigger errors as the records are not properly populated.
# We also prevent sync of other user recurrent events.
return [('calendar_event_ids.user_id', '=', self.env.user.id), ('rrule', '!=', False)]
@api.model
def _odoo_values(self, google_recurrence, default_reminders=()):
return {
'rrule': google_recurrence.rrule,
'google_id': google_recurrence.id,
}
def _google_values(self):
event = self._get_first_event()
if not event:
return {}
values = event._google_values()
values['id'] = self.google_id
if not self._is_allday():
values['start']['timeZone'] = self.event_tz or 'Etc/UTC'
values['end']['timeZone'] = self.event_tz or 'Etc/UTC'
# DTSTART is not allowed by Google Calendar API.
# Event start and end times are specified in the start and end fields.
rrule = re.sub('DTSTART:[0-9]{8}T[0-9]{1,8}\\n', '', self.rrule)
# UNTIL must be in UTC (appending Z)
# We want to only add a 'Z' to non UTC UNTIL values and avoid adding a second.
# 'RRULE:FREQ=DAILY;UNTIL=20210224T235959;INTERVAL=3 --> match UNTIL=20210224T235959
# 'RRULE:FREQ=DAILY;UNTIL=20210224T235959 --> match
rrule = re.sub(r"(UNTIL=\d{8}T\d{6})($|;)", r"\1Z\2", rrule)
values['recurrence'] = ['RRULE:%s' % rrule] if 'RRULE:' not in rrule else [rrule]
property_location = 'shared' if event.user_id else 'private'
values['extendedProperties'] = {
property_location: {
'%s_odoo_id' % self.env.cr.dbname: self.id,
},
}
return values