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

View file

@ -2,12 +2,13 @@ import uuid
from odoo import models, api, fields
import caldav
import logging
from datetime import datetime
from icalendar import Calendar, Event, vCalAddress, Alarm, vText, vWeekday
from datetime import datetime, timezone
from icalendar import Calendar, Event, vCalAddress, vText, vWeekday
from bs4 import BeautifulSoup
from datetime import timedelta
import dateutil
import re
from pytz import timezone, utc
_logger = logging.getLogger(__name__)
@ -143,16 +144,32 @@ class CalendarEvent(models.Model):
calendar.add('version', '2.0')
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.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:
ical_event.add('summary', event.name)
if event.description:
ical_event.add('description', event.description)
if event.description and self._html_to_text(event.description):
ical_event.add('description', self._html_to_text(event.description))
if 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
if event.recurrency and event.recurrence_id:
rrule = event.recurrence_id._get_rrule()
@ -160,8 +177,8 @@ class CalendarEvent(models.Model):
ical_event.add('rrule', rrule_dict)
# Add DTSTART and DTEND
ical_event.add('dtstart', event.start.replace(tzinfo=None))
ical_event.add('dtend', event.stop.replace(tzinfo=None))
ical_event.add('dtstart', utc.localize(event.start).astimezone(user_tz))
ical_event.add('dtend', utc.localize(event.stop).astimezone(user_tz))
calendar.add_component(ical_event)
@ -269,7 +286,6 @@ class CalendarEvent(models.Model):
def sync_event_from_ical(self, ical_event):
email_regex = re.compile(r'[a-z0-9\.\-+_]+@[a-z0-9\.\-+_]+\.[a-z]+')
now = datetime.now()
current_user_email = self.env.user.email.lower()
for component in ical_event.subcomponents:
@ -300,16 +316,18 @@ class CalendarEvent(models.Model):
existing_instance = self._get_existing_instance(uid, recurrence_id)
start = component.decoded('dtstart')
if isinstance(start, datetime):
start = start.replace(tzinfo=None)
start = start.astimezone(utc).replace(tzinfo=None)
end = component.decoded('dtend')
if isinstance(end, datetime):
end = end.replace(tzinfo=None)
end = end.astimezone(utc).replace(tzinfo=None)
values = {
'name': str(component.get('summary')),
'start': start,
'stop': end,
'description': str(component.get('description')),
'location': str(component.get('location')),
'description': self._extract_component_text(component, 'description'),
'location': self._extract_component_text(component, 'location'),
'videocall_location': self._extract_component_text(component, 'conference'),
'caldav_uid': uid,
'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)
@staticmethod
def _extract_component_text(component, subcomponent_name):
text = str(component.get(subcomponent_name))
text = text if text != 'None' else ''
@staticmethod
def _html_to_text(html):