Skip to content

Commit

Permalink
Merge pull request #2 from coopdevs/14.0-feature/sync
Browse files Browse the repository at this point in the history
Sync updated and deleted events with external calendars
  • Loading branch information
oyale committed Mar 31, 2023
2 parents e2cb1a3 + 3d3601f commit de39fc5
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 39 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.devenv
15 changes: 15 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Webcal Exporter

This module allows you to export Odoo calendar events to an external webcal (such as Nextcloud) for each user. Users can provide their webcal URL and credentials in their user profile, and the module will automatically export events created within the last hour. A scheduled action runs hourly to check for recent events and export them to the corresponding user's webcal.

Updated or deleted events trigger an update or deletion in the webcal.

**Note:** This module is not really synchronizing events, but rather exporting them to a webcal. This means that events created in the webcal will not be imported back into Odoo.

**Table of contents**

.. contents::
Expand All @@ -47,6 +51,17 @@ Once the user's calendar credentials are set up correctly, the module will autom
Changelog
=========

14.0.1.0.3 (2023-03-31)
~~~~~~~~~~~~~~~~~~~~~~~

**Features**

- Automatic syncing on edit or unlink
- Create #unsync tag on demand
- Store uuid in calendar.event
- Use uuid as a unique identificator for events :bulb:


14.0.1.0.2 (2023-03-30)
~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
15 changes: 15 additions & 0 deletions webcal_exporter/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Webcal Exporter

This module allows you to export Odoo calendar events to an external webcal (such as Nextcloud) for each user. Users can provide their webcal URL and credentials in their user profile, and the module will automatically export events created within the last hour. A scheduled action runs hourly to check for recent events and export them to the corresponding user's webcal.

Updated or deleted events trigger an update or deletion in the webcal.

**Note:** This module is not really synchronizing events, but rather exporting them to a webcal. This means that events created in the webcal will not be imported back into Odoo.

**Table of contents**

.. contents::
Expand All @@ -47,6 +51,17 @@ Once the user's calendar credentials are set up correctly, the module will autom
Changelog
=========

14.0.1.0.3 (2023-03-31)
~~~~~~~~~~~~~~~~~~~~~~~

**Features**

- Automatic syncing on edit or unlink
- Create #unsync tag on demand
- Store uuid in calendar.event
- Use uuid as a unique identificator for events :bulb:


14.0.1.0.2 (2023-03-30)
~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
5 changes: 3 additions & 2 deletions webcal_exporter/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
'name': 'Webcal Exporter',
'version': '14.0.1.0.2',
'version': '14.0.1.0.3',
'category': 'Extra Tools',
'summary': 'Export Odoo calendar events to external webcal',
'author': 'Coopdevs',
Expand All @@ -9,10 +9,11 @@
'depends': ['base', 'calendar'],
'data': [
'data/ir_cron_data.xml',
'data/ir_actions_server_data.xml',
'views/res_users_view.xml',
],
'external_dependencies': {
'python': ['ics', 'requests', 'pytz'],
'python': ['ics', 'requests', 'pytz', 'caldav'],
},
'installable': True,
'application': False,
Expand Down
32 changes: 32 additions & 0 deletions webcal_exporter/data/ir_actions_server_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<!-- Automated Actions -->
<record id="calendar_event_update_rule" model="base.automation">
<field name="name">Update Event in External Calendar</field>
<field name="model_id" ref="calendar.model_calendar_event" />
<field name="trigger">on_write</field>
<field name="state">code</field>
<field name="code">
for record in records:
if record.external_uuid:
record.update_event_in_external_calendar()
</field>
<field name="active" eval="True" />
</record>

<record id="calendar_event_delete_rule" model="base.automation">
<field name="name">Delete Event in External Calendar</field>
<field name="model_id" ref="calendar.model_calendar_event" />
<field name="trigger">on_unlink</field>
<field name="state">code</field>
<field name="code">
for record in records:
if record.external_uuid:
record.delete_event_in_external_calendar()
</field>
<field name="active" eval="True" />
</record>

</data>
</odoo>
1 change: 1 addition & 0 deletions webcal_exporter/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import res_users
from . import calendar_event
97 changes: 97 additions & 0 deletions webcal_exporter/models/calendar_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import requests
import caldav
import logging

from calendar import Calendar
from requests.auth import HTTPBasicAuth
from odoo import models, fields, api
from datetime import datetime
from pytz import timezone
from icalendar import Calendar, vDatetime

_logger = logging.getLogger(__name__)

class CalendarEvent(models.Model):
_inherit = 'calendar.event'

external_uuid = fields.Char(string='External UUID')

def add_unsynced_tag(self):
unsynced_tag_name = '#unsynced'
category = self.env['calendar.event.type']
unsynced_tag = category.search([('name', '=', unsynced_tag_name)])
if not unsynced_tag:
unsynced_tag = category.create({'name': unsynced_tag_name})
self.categ_ids = [(4, unsynced_tag.id)]

def update_event_in_external_calendar(self):
for event in self:
# Obtén los asistentes al evento antes y después de la actualización
partner_before_update = event._origin.partner_id
partner_after_update = event.partner_id
all_partners = partner_before_update | partner_after_update

for partner in all_partners:
user = self.env['res.users'].search(
[('partner_id', '=', partner.id)], limit=1)
if not user or not user.calendar_credentials_verified:
continue
client = caldav.DAVClient(
url=user.calendar_url, username=user.calendar_user, password=user.calendar_password)
principal = client.principal()
calendars = principal.calendars()

if calendars:
calendar = calendars[0]

external_event = calendar.event_by_uid(event.external_uuid)
if external_event:
ical = Calendar.from_ical(external_event.data)
ical_event = ical.walk('VEVENT')[0]
ical_event['SUMMARY'] = event.name
ical_event['DTSTART'] = vDatetime(event.start.replace(tzinfo=timezone('UTC')))
ical_event['DTEND'] = vDatetime(event.stop.replace(tzinfo=timezone('UTC')))
ical_event['LOCATION'] = event.location or ''
ical_event['DESCRIPTION'] = event.description or '' # Update the event in the external calendar
external_event.data = ical.to_ical()
external_event.save(no_overwrite=False)

def delete_event_in_external_calendar(self):
for event in self:
if not event.external_uuid:
continue

partner = event.partner_id
user = self.env['res.users'].search(
[('partner_id', '=', partner.id)], limit=1)

if not user or not user.calendar_credentials_verified:
continue
base_url = user.calendar_url
calendar_user = user.calendar_user
calendar_password = user.calendar_password
user_sync = user.calendar_credentials_verified

if base_url and calendar_user and calendar_password and user_sync:
event_url = base_url + event.external_uuid + ".ics"
try:
self._delete_ical_event(
event_url, calendar_user, calendar_password)
except Exception as e:
_logger.error(
"Error deleting event from external calendar: %s", str(e))
event.add_unsynced_tag()

def _delete_ical_event(self, event_url, calendar_user, calendar_password):
try:
response = requests.request(
'DELETE', event_url, auth=HTTPBasicAuth(calendar_user, calendar_password), timeout=10
)
_logger.debug("Response: %s %s" %
(response.status_code, response.reason))
if response.status_code not in (204, 404):
response.raise_for_status()
except requests.exceptions.RequestException as e:
_logger.error("Error deleting event: %s %s" %
(response.status_code, response.reason))
raise e
39 changes: 20 additions & 19 deletions webcal_exporter/models/res_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import logging
import pytz
import requests
import time
import uuid

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -82,7 +82,7 @@ def check_credentials(self, url, user, password):
_logger.exception("Error while checking credentials: %s", str(e))
return False

def _publish_ical_event(self, url, user, password, calendar, log):
def _publish_ical_event(self, url, user, password, calendar):
headers = {
'Content-Type': 'text/calendar',
'User-Agent': 'Python-Requests',
Expand All @@ -103,20 +103,22 @@ def _publish_ical_event(self, url, user, password, calendar, log):

def export_recent_events(self):
env = self.env
log = _logger.info
# Calculate the date and time of one hour ago
one_hour_ago = datetime.now() - timedelta(hours=1)

# Search for events created in the last hour
events = env['calendar.event'].search([
('create_date', '>=', one_hour_ago.strftime('%Y-%m-%d %H:%M:%S')),
])
_logger.info("Found %s events to export" % len(events))
_logger.debug("Found %s events to export" % len(events))
# Publish each event to the corresponding user's external calendar
for event_id in events:
_logger.info("Exporting event %s" % event_id.id)
_logger.debug("Exporting event %s" % event_id.id)
event = env['calendar.event'].browse(event_id.id)

if event.external_uuid:
_logger.debug("Event %s already exported" % event_id.id)
continue
event_uuid = str(uuid.uuid4())
for partner in event.partner_ids:
user = env['res.users'].search(
[('partner_id', '=', partner.id)], limit=1)
Expand All @@ -127,22 +129,21 @@ def export_recent_events(self):
event.start).astimezone(user_tz)
event_end = pytz.utc.localize(
event.stop).astimezone(user_tz)
# Get the URL and credentials of the user's calendar
base_url = user.calendar_url
calendar_user = user.calendar_user
calendar_password = user.calendar_password

# If the user has the necessary information for their calendar, publish the event
if base_url and calendar_user and calendar_password:
# If the user has the necessary information for their calendar, try to publish the event
if user.calendar_url and user.calendar_user and user.calendar_password:
calendar = Calendar()
ics_event = Event()
ics_event.name = event.name
ics_event.begin = event_start
ics_event.end = event_end
ics_event.uid = event_uuid
calendar.events.add(ics_event)
timestamp = str(time.time()).replace('.', '')
event_url = base_url + timestamp + "_" + event.name + '.ics'
self._publish_ical_event(
event_url, calendar_user, calendar_password, calendar, log)

return len(events)
event_url = user.calendar_url + ics_event.uid + ".ics"
try:
self._publish_ical_event(
event_url, user.calendar_user, user.calendar_password, calendar)
event.external_uuid = event_uuid
event._origin.write({'external_uuid': event_uuid})
except Exception as e:
_logger.error("Error publishing event to %s" % event_url)
event.add_unsynced_tag()
4 changes: 4 additions & 0 deletions webcal_exporter/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
This module allows you to export Odoo calendar events to an external webcal (such as Nextcloud) for each user. Users can provide their webcal URL and credentials in their user profile, and the module will automatically export events created within the last hour. A scheduled action runs hourly to check for recent events and export them to the corresponding user's webcal.

Updated or deleted events trigger an update or deletion in the webcal.

**Note:** This module is not really synchronizing events, but rather exporting them to a webcal. This means that events created in the webcal will not be imported back into Odoo.
11 changes: 11 additions & 0 deletions webcal_exporter/readme/HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
14.0.1.0.3 (2023-03-31)
~~~~~~~~~~~~~~~~~~~~~~~

**Features**

- Automatic syncing on edit or unlink
- Create #unsync tag on demand
- Store uuid in calendar.event
- Use uuid as a unique identificator for events :bulb:


14.0.1.0.2 (2023-03-30)
~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
Loading

0 comments on commit de39fc5

Please sign in to comment.