# Copyright 2021 Le Filament (https://le-filament.com) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import logging from netbluemind.calendar.api.ICalendarUids import ICalendarUids 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" """ This inheriting class adds some fields and methods to res.users in order to store information for connecting user to Bluemind : * bluemind_login / bluemind_password * is_bm_connection_ok : boolean set when connection was successful * bluemind_user_id : to store the identifier of the user in Bluemind * bluemind_calendar_id : to store the main calendar id linked to user * last_sync_version : last version of calendar synced (Bluemind increments a versionning sequence at each change on calendar) """ 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="_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) @api.depends("company_id.bluemind_url", "bluemind_login", "bluemind_password") def bluemind_auth(self, force=False): """ This method connect to Bluemind API and returns the client object This authentication is only performed if all the above conditions are met: * bluemind_url is set on company * bluemind_login and bluemind_password are set on user * connection was already successful with provided creds or force parameter is given (allowing to make first auth) - this last condition is here to avoid locking the account in case the creds are incorrect, since this method is used in most other methods to retrieve client object """ self.ensure_one() 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: better manage exception except (ServerFault, Exception) as e: self.is_bm_connection_ok = False if force: raise UserError( _( "Something went wrong during connection : [%s]", exception_to_unicode(e), ) ) return False def force_auth(self): """ This methods force authentication (or re-auth) mechanism with creds stored on user It is called from button on user view """ # TODO : add notification that it works or not return self.bluemind_auth(force=True) @api.depends( "company_id.bluemind_domain", "bluemind_login", "bluemind_password", "is_bm_connection_ok", ) def _compute_bluemind_user_id(self): """ This method retrieves the user id from Bluemind client object """ for user in self: if ( user.company_id.bluemind_url and user.bluemind_login and user.bluemind_password and user.is_bm_connection_ok ): client = user.bluemind_auth() if client: user.bluemind_user_id = ( client.directory(user.company_id.bluemind_domain) .getByEmail(user.bluemind_login) .entryUid ) @api.depends("bluemind_user_id") def _compute_bluemind_calendar_uid(self): """ This method retrieves default calendar uid from Bluemind for the user """ for user in self: client_bm = user.bluemind_auth() if client_bm and user.bluemind_user_id: user.bluemind_calendar_id = ICalendarUids( client_bm.apiKey, client_bm.baseUrl ).getDefaultUserCalendar(user.bluemind_user_id) def sync_bluemind_calendar(self): """ This methods retrieves last calendar changes from Bluemind These charges are grouped in 3 objects : * created : all events created since last sync * updated : all events updated since last sync * deleted : all events deleted since last sync Then it creates/updates/deletes Odoo events accordingly The parti pris here is the following : every event in Bluemind calendar is recreated in Odoo with organizer = user """ self.ensure_one() # Retrieve emails from all internal & active Odoo users odoo_users_emails = ( self.sudo() .env["res.users"] .search([("share", "=", False), ("active", "=", True)]) .mapped("partner_id.email") ) # TODO: add checks and error handling # Retrieve all events modified since last sync (followed by self.bluemind_id_version) bm_calendar = self.bluemind_auth().calendar(self.bluemind_calendar_id) # TODO : avoid retrieving all events from beginning, limit to 3 months in past ? # The following function actually retrieves only Blemind event ids, not the full events bm_changeset = bm_calendar.changeset(self.last_sync_version) bm_created_uids = bm_changeset.created bm_updated_uids = bm_changeset.updated bm_deleted_uids = bm_changeset.deleted bm_last_version = bm_changeset.version # Retrieve all events from Odoo with a bluemind_id # (= already synced at least once with Bluemind) Calendar = self.env["calendar.event"] odoo_events_bm_linked = Calendar.search([("bluemind_id", "!=", False)]) odoo_events_bm_uids = odoo_events_bm_linked.mapped("bluemind_id") # Calendar entries created on Bluemind side, not already in Odoo # The events already in Odoo are ignored (probably already synced previously if bm_created_uids: events_to_create = [] bm_events_to_create_uids = [ e for e in bm_created_uids if e not in odoo_events_bm_uids ] # Retrieve remaining full events from Bluemind bm_events_to_create = bm_calendar.multipleGet(bm_events_to_create_uids) # Create corresponding events in Odoo for bm_event in bm_events_to_create: # Only if Organizer is the same as Odoo user or if is not linked to # any active Odoo internal user if ( bm_event.value.main.organizer and (bm_event.value.main.organizer.mailto == self.partner_id.email or bm_event.value.main.organizer.mailto not in odoo_users_emails) ): events_to_create.append(Calendar._bm_to_odoo_values(bm_event)) if events_to_create: Calendar.create(events_to_create) # Calendar entries that have been updated on Bluemind # Retrieve full events from Bluemind if bm_updated_uids: bm_events_to_update = bm_calendar.multipleGet(bm_updated_uids) # Update corresponding events for bm_event in bm_events_to_update: odoo_event = odoo_events_bm_linked.filtered_domain( [("bluemind_id", "=", bm_event.uid)] ) # If related event exists in Odoo and belongs to the user, update it if odoo_event: if odoo_event.user_id == self: odoo_event.update_odoo_event_from_bm(bm_event) # If event does not exist in Odoo log an info else: _logger.info( "Event %s updated in Bluemind does not exist in Odoo", bm_event.uid ) # Calendar entries that have been deleted on Bluemind to be deleted on Odoo # (only if belongs to user) if bm_deleted_uids: odoo_events_to_delete = odoo_events_bm_linked.filtered_domain( [("bluemind_id", "in", bm_deleted_uids), ("user_id", "=", self.id)] ) if odoo_events_to_delete: odoo_events_to_delete.unlink(from_bluemind=True) # Update user last_sync_version with the latest one retrieved from Bluemind self.last_sync_version = bm_last_version # TODO : sync all Odoo events without bluemind_id # Should we sync only events on which user_id is the current user # Or also all events to which he was invited as participant ? # First we would sync only the ones where he is user_id # (because otherwise you would set bluemind_id on an event which # does not belong to the current user, and may have already been synced # - with different bluemind_id by the organizer (user_id)) odoo_events_no_bm = Calendar.search( [("user_id", "=", self.id), ("bluemind_id", "=", False)] ) odoo_events_no_bm.create_odoo_events_in_bm() @api.model def _sync_all_bm_calendar(self): """ Cron job to sync calendars for all users with is_bm_connection_ok """ users = self.env["res.users"].search([("is_bm_connection_ok", "!=", False)]) for user in users: _logger.info("Calendar Synchro - Starting synchronization for %s", user) try: user.with_user(user).sync_bluemind_calendar() _logger.info( "Calendar Synchro - Completed synchronization for %s", user ) except Exception as e: _logger.exception( "[%s] Calendar Synchro - Exception : %s !", user, exception_to_unicode(e), )