diff --git a/models/calendar_event.py b/models/calendar_event.py index fa46866756b2b31095ba1b740ef327a2b79541f6..3a21af54e9ac9e1289e4d48249fefeae78a0c6ce 100644 --- a/models/calendar_event.py +++ b/models/calendar_event.py @@ -1,124 +1,122 @@ # 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 requests -from datetime import timedelta +import os + +from dateutil.parser import parse from dateutil.relativedelta import relativedelta -from netbluemind.python.client import BMClient -from netbluemind.core.api.date.BmDateTime import BmDateTime -from netbluemind.core.api.date.BmDateTimePrecision import BmDateTimePrecision +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.VEventChangesItemDelete import VEventChangesItemDelete from netbluemind.calendar.api.VEventSeries import VEventSeries -from netbluemind.calendar.api.VEvent import VEvent -from netbluemind.calendar.api.VEventQuery import VEventQuery -from netbluemind.icalendar.api.ICalendarElementClassification import ICalendarElementClassification +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.icalendar.api.ICalendarElementRRuleFrequency import ( + ICalendarElementRRuleFrequency, +) +from netbluemind.icalendar.api.ICalendarElementRRuleWeekDay import ( + ICalendarElementRRuleWeekDay, +) from netbluemind.python.client import ServerFault - from pytz import timezone -from dateutil.parser import parse -from odoo import api, fields, models, _ -from odoo.exceptions import UserError -from odoo.loglevels import exception_to_unicode +from odoo import api, fields, models _logger = logging.getLogger(__name__) - PRIVACY_CONVERTER_B2O = { - 'Public': 'public', - 'Private': 'private', - 'Confidential': 'confidential' + "Public": "public", + "Private": "private", + "Confidential": "confidential", } PRIVACY_CONVERTER_O2B = { - 'public': 'Public', - 'private': 'Private', - 'confidential': 'Confidential' + "public": "Public", + "private": "Private", + "confidential": "Confidential", } + class CalendarEvent(models.Model): - _inherit = 'calendar.event' + _inherit = "calendar.event" bluemind_id = fields.Char("Bluemind Event ID") def _bm_to_odoo_values(self, bm_event): # General 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 + "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 - if bm_event.value.main.dtstart.precision.value == 'Date': + 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 - }) + data.update({"allday": True, "start_date": start, "stop_date": stop}) else: timeZone_start = timezone(bm_event.value.main.dtstart.timezone) timeZone_stop = timezone(bm_event.value.main.dtend.timezone) - start = parse(bm_event.value.main.dtstart.iso8601).astimezone(timeZone_start).replace(tzinfo=None) - stop = parse(bm_event.value.main.dtend.iso8601).astimezone(timeZone_stop).replace(tzinfo=None) - data.update({ - 'allday': False, - 'start': start, - 'stop': stop - }) + start = ( + parse(bm_event.value.main.dtstart.iso8601) + .astimezone(timeZone_start) + .replace(tzinfo=None) + ) + stop = ( + parse(bm_event.value.main.dtend.iso8601) + .astimezone(timeZone_stop) + .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 - }) + data.update( + { + "recurrency": True, + "rrule_type": rrule.frequency.name.lower(), + "interval": rrule.interval, + } + ) if rrule.count: - data.update({ - 'end_type': 'count', - 'count': rrule.count - }) + data.update({"end_type": "count", "count": rrule.count}) elif rrule.until: - data.update({ - 'end_type': 'end_date', - 'until': rrule.until.iso8601[:10] - }) + data.update({"end_type": "end_date", "until": rrule.until.iso8601[:10]}) else: - data.update({ - 'end_type': 'forever' - }) + 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) - }) + 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 - }) + 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, - }) + data.update( + { + "weekday": rrule.byDay[0].day, + rrule.byDay[0].day.lower(): True, + } + ) return data def _odoo_to_bm_values(self, event): @@ -126,80 +124,103 @@ class CalendarEvent(models.Model): bm_event.uid = self.generate_uid_bluemind() bm_event.value = VEventSeries() bm_event.value.main = VEvent() - bm_event.value.main.classification = ICalendarElementClassification() + bm_event.value.main.classification = ICalendarElementClassification( + PRIVACY_CONVERTER_O2B.get(event.privacy) + ) bm_event.value.main.dtstart = BmDateTime() - bm_event.value.main.dtstart.precision = BmDateTimePrecision() bm_event.value.main.dtend = BmDateTime() - bm_event.value.main.dtend.precision = BmDateTimePrecision() bm_event.sendNotification = False bm_event.value.main.summary = event.name - bm_event.value.main.description = event.description or '' - bm_event.value.main.location = event.location or '' - bm_event.value.main.classification.value = PRIVACY_CONVERTER_O2B.get(event.privacy) + bm_event.value.main.description = event.description or "" + bm_event.value.main.location = event.location or "" + + # These fields are required (although not marked as such in doc / code) + # Otherwise you get a NullPointerException + bm_event.value.main.attendees = [] + bm_event.value.main.categories = [] + bm_event.value.main.attachments = [] if event.allday: - bm_event.value.main.dtstart.iso8601 = event.start_date.to_string() - bm_event.value.main.dtstart.precision.value = 'Date' + bm_event.value.main.dtstart.iso8601 = event.start_date.isoformat() + bm_event.value.main.dtstart.precision = BmDateTimePrecision("Date") stop_date = event.stop_date + relativedelta(days=1) - bm_event.value.main.dtend.iso8601 = stop_date.strftime("%Y-%m-%d") - bm_event.value.main.dtend.precision.value = 'Date' + bm_event.value.main.dtend.iso8601 = stop_date.isoformat() + bm_event.value.main.dtend.precision = BmDateTimePrecision("Date") else: - bm_event.value.main.dtstart.iso8601 = event.start.strftime("%Y-%m-%dT%H:%M:%S") - bm_event.value.main.dtstart.precision.value = 'DateTime' - bm_event.value.main.dtend.iso8601 = event.stop.strftime("%Y-%m-%dT%H:%M:%S") - bm_event.value.main.dtend.precision.value = 'DateTime' - - bm_event.value.main.dtstart.timezone = timezone(event.event_tz) - bm_event.value.main.dtend.timezone = timezone(event.event_tz) + tz = timezone(self.env.context.get("tz")) + bm_event.value.main.dtstart.iso8601 = event.start.astimezone(tz).isoformat( + timespec="milliseconds" + ) + bm_event.value.main.dtstart.timezone = tz.zone + bm_event.value.main.dtstart.precision = BmDateTimePrecision("DateTime") + bm_event.value.main.dtend.iso8601 = event.stop.astimezone(tz).isoformat( + timespec="milliseconds" + ) + bm_event.value.main.dtend.timezone = tz.zone + bm_event.value.main.dtend.precision = BmDateTimePrecision("DateTime") # Recurrency Handling if event.recurrency: bm_event.value.main.rrule = ICalendarElementRRule() - bm_event.value.main.rrule.frequency = ICalendarElementRRuleFrequency() - bm_event.value.main.rrule.frequency.value = event.frequency.toupper() + bm_event.value.main.rrule.frequency = ICalendarElementRRuleFrequency( + event.frequency.toupper() + ) bm_event.value.main.rrule.interval = event.interval - if end_type == "count": + if event.end_type == "count": bm_event.value.main.rrule.count = event.count - elif end_type == "date_end": + elif event.end_type == "end_date": bm_event.value.main.rrule.until = BmDateTime() - bm_event.value.main.rrule.until.iso8601 = event.until.strftime("%Y-%m-%d") - bm_event.value.main.rrule.until.precision = BmDateTimePrecision() - bm_event.value.main.rrule.until.precision.value = 'Date' + bm_event.value.main.rrule.until.iso8601 = event.until.strftime( + "%Y-%m-%d" + ) + bm_event.value.main.rrule.until.precision = BmDateTimePrecision("Date") bm_event.value.main.rrule.until.timezone = event.event_tz - if (event.frequency == "monthly" and event.month_by == "day") or event.frequency == "weekly": - bm_event.value.main.rrule.byDay = [ICalendarElementRRuleWeekDay()] - bm_event.value.main.rrule.byDay[0].day = str(event.weekday) - bm_event.value.main.rrule.byDay[0].offset = int(event.byday) + if ( + event.frequency == "monthly" and event.month_by == "day" + ) or event.frequency == "weekly": + bm_event.value.main.rrule.byDay = [ICalendarElementRRuleWeekDay()] + bm_event.value.main.rrule.byDay[0].day = str(event.weekday) + bm_event.value.main.rrule.byDay[0].offset = int(event.byday) elif event.frequency == "monthly" and event.month_by == "date": bm_event.value.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)) + '-'.encode() \ - + binascii.b2a_hex(os.urandom(2)) + '-'.encode() \ - + binascii.b2a_hex(os.urandom(2)) + '-'.encode() \ - + binascii.b2a_hex(os.urandom(2)) + '-'.encode() \ + """ + 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 + ) + return uid.decode() # TODO: Create function to update Odoo event from Bluemind updated event def update_odoo_event_from_bm(self, odoo_event, bm_event): return NotImplementedError # TODO: add function to update Bluemind object when Odoo object is modified + # 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): return super().write(values) + @api.model_create_multi def create(self, vals_list): odoo_events = super().create(vals_list) bm_events = VEventChanges() + bm_events.add = [] for odoo_event in odoo_events: if not odoo_event.bluemind_id: bm_event = self._odoo_to_bm_values(odoo_event) @@ -207,24 +228,28 @@ class CalendarEvent(models.Model): odoo_event.bluemind_id = bm_event.uid if bm_events.add: try: - bm_calendar = self.env.user.bluemind_auth().calendar(self.bluemind_calendar_id) + bm_calendar = self.env.user.bluemind_auth().calendar( + self.env.user.bluemind_calendar_id + ) bm_calendar.updates(bm_events) - except (ServerFault) as e: + except (ServerFault, Exception) as e: # TODO: better manage exceptions - print(e) + _logger.warning( + "Did not manage to push events to Bluemind, error [%s]", e + ) return odoo_events def unlink(self, from_bluemind=False): - '''Unlink override, deletes an event from Odoo to BlueMind''' + """Unlink override, deletes an event from Odoo to BlueMind""" if not from_bluemind: for event in self: if event.bluemind_id: - bm_calendar = self.env.user.bluemind_auth().calendar(self.bluemind_calendar_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) as e: + except (ServerFault): # TODO: better manage exceptions event.active = False - return super( - CalendarEvent, - self.filtered(lambda e: e.active is True)).unlink() + return super(CalendarEvent, self.filtered(lambda e: e.active is True)).unlink() diff --git a/models/res_users.py b/models/res_users.py index f791deadb93480c2a4c84754d79432a3b88247e6..0bd3044e456f2da86c0ccfe8949d4feeedaabb9b 100644 --- a/models/res_users.py +++ b/models/res_users.py @@ -2,50 +2,67 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import logging -import requests -from datetime import timedelta -from netbluemind.python.client import BMClient -from netbluemind.python.client import ServerFault -from odoo import api, fields, models, _ +from netbluemind.python.client import BMClient, ServerFault + +from odoo import _, api, fields, models from odoo.exceptions import UserError -from odoo.loglevels import exception_to_unicode _logger = logging.getLogger(__name__) + class ResUser(models.Model): - _inherit = 'res.users' + _inherit = "res.users" bluemind_login = fields.Char("Bluemind e-mail") bluemind_password = fields.Char("Bluemind password or API key") is_bm_connection_ok = fields.Boolean("Bluemind Connection OK", default=False) - bluemind_user_id = fields.Char("Bluemind user ID", compute='_retrieve_bluemind_user_id', store=True) - bluemind_calendar_id = fields.Char("Bluemind calendar ID", compute='_compute_bluemind_calendar_uid', store=True) + bluemind_user_id = fields.Char( + "Bluemind user ID", compute="_compute_bluemind_user_id", store=True + ) + bluemind_calendar_id = fields.Char( + "Bluemind calendar ID", compute="_compute_bluemind_calendar_uid", store=True + ) last_sync_version = fields.Integer("Last sync version", readonly=True, default=0) # Authentication to Bluemind @api.depends("company_id.bluemind_url", "bluemind_login", "bluemind_password") - def bluemind_auth(self): + def bluemind_auth(self, force=False): self.ensure_one() - - if self.company_id.bluemind_url and self.bluemind_login and self.bluemind_password: + if ( + self.company_id.bluemind_url + and self.bluemind_login + and self.bluemind_password + and (self.is_bm_connection_ok or force) + ): try: client = BMClient(self.company_id.bluemind_url + "/api") client.login(self.bluemind_login, self.bluemind_password) self.is_bm_connection_ok = True return client - # TODO: add boolean if connection is OK, in order not to sync users with NOK connection # TODO: better manage exception except (ServerFault, Exception) as e: - raise UserError(_("Something went wrong during connection : [%s]", e)) self.is_bm_connection_ok = False + if force: + raise UserError( + _("Something went wrong during connection : [%s]", e) + ) return False @api.depends("company_id.bluemind_domain", "bluemind_login", "bluemind_password") - def _retrieve_bluemind_user_id(self): + def _compute_bluemind_user_id(self): for user in self: - if user.company_id.bluemind_url and user.bluemind_login and user.bluemind_password: - user.bluemind_user_id = user.bluemind_auth().directory(user.company_id.bluemind_domain).getByEmail(user.bluemind_login).entryUid + if ( + user.company_id.bluemind_url + and user.bluemind_login + and user.bluemind_password + ): + user.bluemind_user_id = ( + user.bluemind_auth() + .directory(user.company_id.bluemind_domain) + .getByEmail(user.bluemind_login) + .entryUid + ) @api.depends("bluemind_user_id") def _compute_bluemind_calendar_uid(self): @@ -66,14 +83,18 @@ class ResUser(models.Model): bm_last_version = bm_changeset.version Calendar = self.env["calendar.event"] - odoo_events_bm_linked = Calendar.search([("user_id", "=", self.id), ("bluemind_id", "!=", False)]) + odoo_events_bm_linked = Calendar.search( + [("user_id", "=", self.id), ("bluemind_id", "!=", False)] + ) odoo_events_bm_uids = odoo_events_bm_linked.mapped("bluemind_id") # Calendar entries created on Bluemind side, not already in Odoo # TODO: move events already in Odoo to updated section ? # May need to distinguish between : first sync ever, new sync after unsync events_to_create = [] - bm_events_to_create_uids = [e for e in bm_created_uids if e not in odoo_events_bm_uids] + bm_events_to_create_uids = [ + e for e in bm_created_uids if e not in odoo_events_bm_uids + ] bm_events_to_create = bm_calendar.multipleGet(bm_events_to_create_uids) for bm_event in bm_events_to_create: events_to_create.append(Calendar._bm_to_odoo_values(bm_event)) @@ -82,11 +103,15 @@ class ResUser(models.Model): # Calendar entries that have been modified on Bluemind bm_events_to_update = bm_calendar.multipleGet(bm_updated_uids) for bm_event in bm_events_to_update: - odoo_event = odoo_events_bm_linked.filtered([("bluemind_id", "=", bm_event.uid)]) + odoo_event = odoo_events_bm_linked.filtered( + [("bluemind_id", "=", bm_event.uid)] + ) Calendar.update_odoo_event_from_bm(odoo_event, bm_event) # Calendar entries that have been deleted on Bluemind to be deleted on Odoo - odoo_events_to_delete = odoo_events_bm_linked.filtered([("bluemind_id", "in", bm_deleted_uids)]) + odoo_events_to_delete = odoo_events_bm_linked.filtered( + [("bluemind_id", "in", bm_deleted_uids)] + ) odoo_events_to_delete.unlink(from_bluemind=True) self.last_sync_version = bm_last_version