Add videoconference sync and fix some bugs:

- Properly synchronize the timezone for users with timezones set in Odoo
- Properly synchronize attendees, i.e. remove the organizer from the
  attendees list on the CalDAV server and re-add them on the event
  attendees in Odoo (to show up in the calendar view).
This commit is contained in:
Marc Durepos 2024-05-28 11:35:24 -04:00
parent 4781b33123
commit 137789cd79
2 changed files with 39 additions and 14 deletions

View file

@ -8,7 +8,7 @@
{ {
'name': 'CalDAV Synchronization', 'name': 'CalDAV Synchronization',
'version': '17.0.0.5.2', 'version': '17.0.0.5.3',
'license': 'LGPL-3', 'license': 'LGPL-3',
'category': 'Productivity', 'category': 'Productivity',
'summary': 'Synchronize Odoo Calendar Events with CalDAV Servers', 'summary': 'Synchronize Odoo Calendar Events with CalDAV Servers',
@ -20,6 +20,9 @@
'author': 'Bemade Inc.', 'author': 'Bemade Inc.',
'website': 'https://www.bemade.org', 'website': 'https://www.bemade.org',
'depends': ['base', 'calendar'], 'depends': ['base', 'calendar'],
'external_dependencies': {
'python': ['caldav', 'icalendar', 'bs4'],
},
'images': ['static/description/images/main_screenshot.png'], 'images': ['static/description/images/main_screenshot.png'],
'data': [ 'data': [
'views/res_users_views.xml', 'views/res_users_views.xml',

View file

@ -2,12 +2,13 @@ import uuid
from odoo import models, api, fields from odoo import models, api, fields
import caldav import caldav
import logging import logging
from datetime import datetime from datetime import datetime, timezone
from icalendar import Calendar, Event, vCalAddress, Alarm, vText, vWeekday from icalendar import Calendar, Event, vCalAddress, vText, vWeekday
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from datetime import timedelta from datetime import timedelta
import dateutil import dateutil
import re import re
from pytz import timezone, utc
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -143,16 +144,32 @@ class CalendarEvent(models.Model):
calendar.add('version', '2.0') calendar.add('version', '2.0')
for event in self: for event in self:
user_tz = timezone('UTC')
if event.user_id.tz:
user_tz = timezone(event.user_id.tz)
ical_event = Event() ical_event = Event()
ical_event.add('uid', event.caldav_uid) ical_event.add('uid', event.caldav_uid)
ical_event.add('dtstamp', event.write_date.replace(tzinfo=None)) ical_event.add('dtstamp', utc.localize(event.write_date).astimezone(user_tz))
if event.name: if event.name:
ical_event.add('summary', event.name) ical_event.add('summary', event.name)
if event.description: if event.description and self._html_to_text(event.description):
ical_event.add('description', event.description) ical_event.add('description', self._html_to_text(event.description))
if event.location: if event.location:
ical_event.add('location', event.location) ical_event.add('location', event.location)
if event.videocall_location:
ical_event.add('CONFERENCE', event.videocall_location)
for partner in event.partner_ids:
if partner == event.user_id.partner_id:
continue
attendee = vCalAddress(f'MAILTO:{partner.email}')
attendee.params['cn'] = vText(partner.name)
attendee_record = self.env['calendar.attendee'].search([('event_id', '=', event.id), ('partner_id', '=', partner.id)], limit=1)
if attendee_record:
attendee.params['partstat'] = vText(self._map_attendee_status(attendee_record.state))
ical_event.add('attendee', attendee, encode=0)
organizer = vCalAddress(f"MAILTO:{event.user_id.email}")
organizer.params['cn'] = event.user_id.name
ical_event.add('organizer', organizer)
# Add RRULE if the event is recurrent # Add RRULE if the event is recurrent
if event.recurrency and event.recurrence_id: if event.recurrency and event.recurrence_id:
rrule = event.recurrence_id._get_rrule() rrule = event.recurrence_id._get_rrule()
@ -160,8 +177,8 @@ class CalendarEvent(models.Model):
ical_event.add('rrule', rrule_dict) ical_event.add('rrule', rrule_dict)
# Add DTSTART and DTEND # Add DTSTART and DTEND
ical_event.add('dtstart', event.start.replace(tzinfo=None)) ical_event.add('dtstart', utc.localize(event.start).astimezone(user_tz))
ical_event.add('dtend', event.stop.replace(tzinfo=None)) ical_event.add('dtend', utc.localize(event.stop).astimezone(user_tz))
calendar.add_component(ical_event) calendar.add_component(ical_event)
@ -269,7 +286,6 @@ class CalendarEvent(models.Model):
def sync_event_from_ical(self, ical_event): def sync_event_from_ical(self, ical_event):
email_regex = re.compile(r'[a-z0-9\.\-+_]+@[a-z0-9\.\-+_]+\.[a-z]+') email_regex = re.compile(r'[a-z0-9\.\-+_]+@[a-z0-9\.\-+_]+\.[a-z]+')
now = datetime.now()
current_user_email = self.env.user.email.lower() current_user_email = self.env.user.email.lower()
for component in ical_event.subcomponents: for component in ical_event.subcomponents:
@ -300,16 +316,18 @@ class CalendarEvent(models.Model):
existing_instance = self._get_existing_instance(uid, recurrence_id) existing_instance = self._get_existing_instance(uid, recurrence_id)
start = component.decoded('dtstart') start = component.decoded('dtstart')
if isinstance(start, datetime): if isinstance(start, datetime):
start = start.replace(tzinfo=None) start = start.astimezone(utc).replace(tzinfo=None)
end = component.decoded('dtend') end = component.decoded('dtend')
if isinstance(end, datetime): if isinstance(end, datetime):
end = end.replace(tzinfo=None) end = end.astimezone(utc).replace(tzinfo=None)
values = { values = {
'name': str(component.get('summary')), 'name': str(component.get('summary')),
'start': start, 'start': start,
'stop': end, 'stop': end,
'description': str(component.get('description')), 'description': self._extract_component_text(component, 'description'),
'location': str(component.get('location')), 'location': self._extract_component_text(component, 'location'),
'videocall_location': self._extract_component_text(component, 'conference'),
'caldav_uid': uid, 'caldav_uid': uid,
'partner_ids': [(6, 0, attendee_ids.ids)], 'partner_ids': [(6, 0, attendee_ids.ids)],
} }
@ -328,6 +346,10 @@ class CalendarEvent(models.Model):
}) })
existing_instance.with_context({'caldav_no_sync': True}).write(values) existing_instance.with_context({'caldav_no_sync': True}).write(values)
@staticmethod
def _extract_component_text(component, subcomponent_name):
text = str(component.get(subcomponent_name))
text = text if text != 'None' else ''
@staticmethod @staticmethod
def _html_to_text(html): def _html_to_text(html):