Sélectionner une révision Git
calendar_event.py 14,30 Kio
# Copyright 2021 Le Filament (https://le-filament.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
import binascii
import logging
import os
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from netbluemind.calendar.api.VEvent import VEvent
from netbluemind.calendar.api.VEventChanges import VEventChanges
from netbluemind.calendar.api.VEventChangesItemAdd import VEventChangesItemAdd
from netbluemind.calendar.api.VEventChangesItemModify import VEventChangesItemModify
from netbluemind.calendar.api.VEventSeries import VEventSeries
from netbluemind.core.api.date.BmDateTime import BmDateTime
from netbluemind.core.api.date.BmDateTimePrecision import BmDateTimePrecision
from netbluemind.icalendar.api.ICalendarElementClassification import (
ICalendarElementClassification,
)
from netbluemind.icalendar.api.ICalendarElementRRule import ICalendarElementRRule
from netbluemind.icalendar.api.ICalendarElementRRuleFrequency import (
ICalendarElementRRuleFrequency,
)
from netbluemind.icalendar.api.ICalendarElementRRuleWeekDay import (
ICalendarElementRRuleWeekDay,
)
from netbluemind.python.client import ServerFault
from pytz import timezone
from odoo import api, fields, models
from odoo.loglevels import exception_to_unicode
_logger = logging.getLogger(__name__)
PRIVACY_CONVERTER_B2O = {
"Public": "public",
"Private": "private",
"Confidential": "confidential",
}
PRIVACY_CONVERTER_O2B = {
"public": "Public",
"private": "Private",
"confidential": "Confidential",
}
# TODO: manage attendee_ids, organizer, alarm_ids, priority, status, categories ?
# TODO: check if recurrency properly working without exceptions
# TODO: manage exceptions in recurrencies
class CalendarEvent(models.Model):
_inherit = "calendar.event"
"""
This inheriting class adds 1 field to calendar.event table :
* bluemind_id = Unique identifier of event in Bluemind
It also adds a number of methods to transform events between Bluemind and Odoo formats
"""
bluemind_id = fields.Char("Bluemind Event ID")
def _bm_to_odoo_values(self, bm_event):
"""
This method converts a bluemind event (given as a parameter) into
a dict that can later be used to create/update an event in Odoo
"""
# Generic fields
data = {
"name": bm_event.value.main.summary,
"bluemind_id": bm_event.uid,
"privacy": PRIVACY_CONVERTER_B2O.get(
bm_event.value.main.classification.value,
self.default_get(["privacy"])["privacy"],
),
"location": bm_event.value.main.location,
"description": bm_event.value.main.description,
"user_id": self.env.user.id,
"create_uid": self.env.user.id,
}
# Dates handling, with timezones
if bm_event.value.main.dtstart.precision.value == "Date":
start = bm_event.value.main.dtstart.iso8601
stop = bm_event.value.main.dtend.iso8601
data.update({"allday": True, "start_date": start, "stop_date": stop})
else:
utc = timezone("UTC")
start = (
parse(bm_event.value.main.dtstart.iso8601)
.astimezone(utc)
.replace(tzinfo=None)
)
stop = (
parse(bm_event.value.main.dtend.iso8601)
.astimezone(utc)
.replace(tzinfo=None)
)
data.update({"allday": False, "start": start, "stop": stop})
# Recurrency handling
rrule = bm_event.value.main.rrule
if rrule:
data.update(
{
"recurrency": True,
"rrule_type": rrule.frequency.name.lower(),
"interval": rrule.interval,
}
)
if rrule.count:
data.update({"end_type": "count", "count": rrule.count})
elif rrule.until:
data.update({"end_type": "end_date", "until": rrule.until.iso8601[:10]})
else:
data.update({"end_type": "forever"})
if rrule.frequency.name == "MONTHLY":
if rrule.byDay:
data.update(
{
"month_by": "day",
"weekday": rrule.byDay[0].day,
rrule.byDay[0].day.lower(): True,
"byday": str(rrule.byDay[0].offset),
}
)
else:
data.update({"month_by": "date", "day": rrule.byMonthDay})
elif rrule.frequency.name == "WEEKLY":
data.update(
{
"weekday": rrule.byDay[0].day,
rrule.byDay[0].day.lower(): True,
}
)
# Returns data dictionary
return data
def _odoo_to_bm_values(self, event):
"""
This method converts an Odoo event (given as a parameter) into
a Bluemind event object that can then be used to create/update an event in Bluemind
"""
# Event object initialization
bm_event = VEventSeries()
bm_event.main = VEvent()
bm_event.main.dtstart = BmDateTime()
bm_event.main.dtend = BmDateTime()
# Event generic information
bm_event.main.summary = event.name
bm_event.main.description = event.description or ""
bm_event.main.location = event.location or ""
bm_event.main.classification = ICalendarElementClassification(
PRIVACY_CONVERTER_O2B.get(event.privacy)
)
# These fields are required (although not marked as such in doc / code)
# Otherwise you get a NullPointerException
bm_event.main.attendees = []
bm_event.main.categories = []
bm_event.main.attachments = []
# Specific case for full day events
if event.allday:
bm_event.main.dtstart.iso8601 = event.start_date.isoformat()
bm_event.main.dtstart.precision = BmDateTimePrecision("Date")
stop_date = event.stop_date + relativedelta(days=1)
bm_event.main.dtend.iso8601 = stop_date.isoformat()
bm_event.main.dtend.precision = BmDateTimePrecision("Date")
# All other cases
else:
tz = timezone(self.env.context.get("tz"))
bm_event.main.dtstart.iso8601 = event.start.astimezone(tz).isoformat(
timespec="milliseconds"
)
bm_event.main.dtstart.timezone = tz.zone
bm_event.main.dtstart.precision = BmDateTimePrecision("DateTime")
bm_event.main.dtend.iso8601 = event.stop.astimezone(tz).isoformat(
timespec="milliseconds"
)
bm_event.main.dtend.timezone = tz.zone
bm_event.main.dtend.precision = BmDateTimePrecision("DateTime")
# Recurrency Handling
if event.recurrency:
bm_event.main.rrule = ICalendarElementRRule()
bm_event.main.rrule.frequency = ICalendarElementRRuleFrequency(
event.frequency.toupper()
)
bm_event.main.rrule.interval = event.interval
if event.end_type == "count":
bm_event.main.rrule.count = event.count
elif event.end_type == "end_date":
bm_event.main.rrule.until = BmDateTime()
bm_event.main.rrule.until.iso8601 = event.until.strftime("%Y-%m-%d")
bm_event.main.rrule.until.precision = BmDateTimePrecision("Date")
bm_event.main.rrule.until.timezone = event.event_tz
if (
event.frequency == "monthly" and event.month_by == "day"
) or event.frequency == "weekly":
bm_event.main.rrule.byDay = [ICalendarElementRRuleWeekDay()]
bm_event.main.rrule.byDay[0].day = str(event.weekday)
bm_event.main.rrule.byDay[0].offset = int(event.byday)
elif event.frequency == "monthly" and event.month_by == "date":
bm_event.main.rrule.byMonthDay = [event.day]
return bm_event
def generate_uid_bluemind(self):
"""
Creates a uid which corresponds to bluemind representation\
(needed for all uplink creation)
"""
uid = (
binascii.b2a_hex(os.urandom(4))
+ b"-"
+ binascii.b2a_hex(os.urandom(2))
+ b"-"
+ binascii.b2a_hex(os.urandom(2))
+ b"-"
+ binascii.b2a_hex(os.urandom(2))
+ b"-"
+ binascii.b2a_hex(os.urandom(6))
)
return uid.decode()
def update_odoo_event_from_bm(self, bm_event):
"""
Update an Odoo event with fields from a Bluemind event (bm_event)
"""
new_odoo_fields = self._bm_to_odoo_values(bm_event)
self.write(new_odoo_fields)
def create_odoo_events_in_bm(self):
"""
Create events from Odoo (odoo_events) in Bluemind
"""
# Create a list of Bluemind events to be created
bm_events = VEventChanges()
bm_events.add = []
for odoo_event in self:
# Avoid recreating events if already linked to Bluemind event
if not odoo_event.bluemind_id:
# Create a Bluemind event object and fills it with values
# from Odoo event
event_add = VEventChangesItemAdd()
event_add.uid = self.generate_uid_bluemind()
event_add.value = self._odoo_to_bm_values(odoo_event)
# Avoids sending notification from Bluemind
# (since assumed already sent from Odoo)
event_add.sendNotification = False
# Add Bluemind event to list
bm_events.add.append(event_add)
# Set bluemind_id on Odoo event (without pushing to Bluemind)
odoo_event.write({"bluemind_id": event_add.uid}, from_bluemind=True)
# If list of Bluemind events to be created is not empty,
# create these events in Bluemind
if bm_events.add:
try:
bm_calendar = self.env.user.bluemind_auth().calendar(
self.env.user.bluemind_calendar_id
)
bm_calendar.updates(bm_events)
except (ServerFault, Exception) as e:
# TODO: better manage exceptions
_logger.warning(
"Did not manage to push events to Bluemind, error [%s]",
exception_to_unicode(e),
)
# Take caution about attendees (only one calendar event in Odoo for all attendees,
# when in Bluemind you get a different one per attendee)
def write(self, values, from_bluemind=False):
"""
Overwrite of write() default method to update Bluemind event everytime
an event is updated in Odoo
A parameter is added "from_bluemind" to avoid updating a Bluemind event
when change is already coming from Bluemind
"""
# Calls base write() function first
result = super().write(values)
# AFAICT Bluemind needs all the fields to be provided,
# you cannot send only the updated ones
if result and not from_bluemind:
# Create a list of Bluemind events to be updated
bm_events = VEventChanges()
bm_events.modify = []
# Only update events with a bluemind_id
for event in self.filtered("bluemind_id"):
if event.bluemind_id:
# Create a Bluemind event object and fills it with values
# from Odoo event
event_change = VEventChangesItemModify()
event_change.uid = event.bluemind_id
event_change.value = self._odoo_to_bm_values(event)
# Avoids sending notification from Bluemind
# (since assumed already sent from Odoo)
event_change.sendNotification = False
# Add Bluemind event to list
bm_events.modify.append(event_change)
# If list of Bluemind events to be updated is not empty,
# update these events in Bluemind
if bm_events.modify:
try:
bm_calendar = self.env.user.bluemind_auth().calendar(
self.env.user.bluemind_calendar_id
)
bm_calendar.updates(bm_events)
except (ServerFault, Exception) as e:
# TODO: better manage exceptions
_logger.warning(
"Did not manage to push events to Bluemind, error [%s]",
exception_to_unicode(e),
)
return result
@api.model_create_multi
def create(self, vals_list):
"""
Overwrite of create() default method to create Bluemind event everytime
an event is created in Odoo
"""
# Calls base create() function first
odoo_events = super().create(vals_list)
self.create_odoo_events_in_bm()
return odoo_events
def unlink(self, from_bluemind=False):
"""
Overwrite of unlink() default method to delete Bluemind event everytime
an event is deleted in Odoo
A parameter is added "from_bluemind" to avoid deleting a Bluemind event
when deletion is already coming from Bluemind
"""
if not from_bluemind:
for event in self:
# If Odoo event is linked to a Bluemind event
if event.bluemind_id:
bm_calendar = self.env.user.bluemind_auth().calendar(
self.env.user.bluemind_calendar_id
)
try:
bm_calendar.delete(event.bluemind_id, False)
except (ServerFault):
# TODO: better manage exceptions
# If deletion fails, keep event in Odoo but archive it
event.active = False
# Calls base unlink() function last only on active events
# (to avoid deleting the Odoo event in case it was just archived)
return super(CalendarEvent, self.filtered(lambda e: e.active is True)).unlink()