From 4208fdddf0f691ad885658f929eb757d319592e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9mi=20-=20Le=20Filament?= <remi@le-filament.com>
Date: Thu, 28 Oct 2021 19:17:56 +0200
Subject: [PATCH] [ADD] write on odoo event pushes to Bluemind

---
 __manifest__.py           |   3 +-
 data/cron_data.xml        |  16 ++++++
 models/calendar_event.py  | 118 +++++++++++++++++++++++---------------
 models/res_users.py       |  27 ++++++++-
 views/res_users_views.xml |  30 +++++++---
 5 files changed, 139 insertions(+), 55 deletions(-)
 create mode 100644 data/cron_data.xml

diff --git a/__manifest__.py b/__manifest__.py
index 545080a..5d83725 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -9,8 +9,9 @@
         "calendar",
     ],
     "data": [
-        #"security/ir.model.access.csv",
+        # "security/ir.model.access.csv",
         # datas
+        "data/cron_data.xml",
         # views
         "views/res_company_views.xml",
         "views/res_users_views.xml",
diff --git a/data/cron_data.xml b/data/cron_data.xml
new file mode 100644
index 0000000..b62470e
--- /dev/null
+++ b/data/cron_data.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+    <data>
+        <record forcecreate="True" id="ir_cron_sync_all_bm_calendars" model="ir.cron">
+            <field name="name">Bluemind: synchronization</field>
+            <field name="model_id" ref="model_res_users" />
+            <field name="state">code</field>
+            <field name="code">model._sync_all_bm_calendar()</field>
+            <field name="user_id" ref="base.user_root" />
+            <field name="interval_number">4</field>
+            <field name="interval_type">hours</field>
+            <field name="numbercall">-1</field>
+            <field eval="False" name="doall" />
+        </record>
+    </data>
+</odoo>
diff --git a/models/calendar_event.py b/models/calendar_event.py
index 3a21af5..6312d65 100644
--- a/models/calendar_event.py
+++ b/models/calendar_event.py
@@ -10,6 +10,7 @@ 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
@@ -27,6 +28,7 @@ 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__)
 
@@ -42,6 +44,8 @@ PRIVACY_CONVERTER_O2B = {
 }
 
 
+# TODO: manage attendee_ids, alarm_ids ?
+# TODO: check if recurrency prperly working
 class CalendarEvent(models.Model):
     _inherit = "calendar.event"
 
@@ -120,71 +124,66 @@ class CalendarEvent(models.Model):
         return data
 
     def _odoo_to_bm_values(self, event):
-        bm_event = VEventChangesItemAdd()
-        bm_event.uid = self.generate_uid_bluemind()
-        bm_event.value = VEventSeries()
-        bm_event.value.main = VEvent()
-        bm_event.value.main.classification = ICalendarElementClassification(
+        bm_event = VEventSeries()
+        bm_event.main = VEvent()
+        bm_event.main.classification = ICalendarElementClassification(
             PRIVACY_CONVERTER_O2B.get(event.privacy)
         )
-        bm_event.value.main.dtstart = BmDateTime()
-        bm_event.value.main.dtend = BmDateTime()
-        bm_event.sendNotification = False
+        bm_event.main.dtstart = BmDateTime()
+        bm_event.main.dtend = BmDateTime()
 
-        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.main.summary = event.name
+        bm_event.main.description = event.description or ""
+        bm_event.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 = []
+        bm_event.main.attendees = []
+        bm_event.main.categories = []
+        bm_event.main.attachments = []
 
         if event.allday:
-            bm_event.value.main.dtstart.iso8601 = event.start_date.isoformat()
-            bm_event.value.main.dtstart.precision = BmDateTimePrecision("Date")
+            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.value.main.dtend.iso8601 = stop_date.isoformat()
-            bm_event.value.main.dtend.precision = BmDateTimePrecision("Date")
+            bm_event.main.dtend.iso8601 = stop_date.isoformat()
+            bm_event.main.dtend.precision = BmDateTimePrecision("Date")
         else:
             tz = timezone(self.env.context.get("tz"))
-            bm_event.value.main.dtstart.iso8601 = event.start.astimezone(tz).isoformat(
+            bm_event.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(
+            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.value.main.dtend.timezone = tz.zone
-            bm_event.value.main.dtend.precision = BmDateTimePrecision("DateTime")
+            bm_event.main.dtend.timezone = tz.zone
+            bm_event.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.main.rrule = ICalendarElementRRule()
+            bm_event.main.rrule.frequency = ICalendarElementRRuleFrequency(
                 event.frequency.toupper()
             )
-            bm_event.value.main.rrule.interval = event.interval
+            bm_event.main.rrule.interval = event.interval
             if event.end_type == "count":
-                bm_event.value.main.rrule.count = event.count
+                bm_event.main.rrule.count = event.count
             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("Date")
-                bm_event.value.main.rrule.until.timezone = event.event_tz
+                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.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)
+                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.value.main.rrule.byMonthDay = [event.day]
+                bm_event.main.rrule.byMonthDay = [event.day]
 
         return bm_event
 
@@ -208,13 +207,38 @@ class CalendarEvent(models.Model):
 
     # TODO: Create function to update Odoo event from Bluemind updated event
     def update_odoo_event_from_bm(self, odoo_event, bm_event):
-        return NotImplementedError
+        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)
+    def write(self, values, from_bluemind=False):
+        # For now this function pushes again the full event to bluemind,
+        # TODO: check if this could be improved to push only a few fields ?
+        result = super().write(values)
+        if result and not from_bluemind:
+            bm_events = VEventChanges()
+            bm_events.modify = []
+            for event in self.filtered("bluemind_id"):
+                if event.bluemind_id:
+                    event_change = VEventChangesItemModify()
+                    event_change.uid = event.bluemind_id
+                    event_change.value = self._odoo_to_bm_values(event)
+                    event_change.sendNotification = False
+                    bm_events.modify.append(event_change)
+            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):
@@ -223,9 +247,12 @@ class CalendarEvent(models.Model):
         bm_events.add = []
         for odoo_event in odoo_events:
             if not odoo_event.bluemind_id:
-                bm_event = self._odoo_to_bm_values(odoo_event)
-                bm_events.add.append(bm_event)
-                odoo_event.bluemind_id = bm_event.uid
+                event_add = VEventChangesItemAdd()
+                event_add.uid = self.generate_uid_bluemind()
+                event_add.value = self._odoo_to_bm_values(odoo_event)
+                event_add.sendNotification = False
+                bm_events.add.append(event_add)
+                odoo_event.write({"bluemind_id": event_add.uid}, from_bluemind=True)
         if bm_events.add:
             try:
                 bm_calendar = self.env.user.bluemind_auth().calendar(
@@ -235,7 +262,8 @@ class CalendarEvent(models.Model):
             except (ServerFault, Exception) as e:
                 # TODO: better manage exceptions
                 _logger.warning(
-                    "Did not manage to push events to Bluemind, error [%s]", e
+                    "Did not manage to push events to Bluemind, error [%s]",
+                    exception_to_unicode(e),
                 )
         return odoo_events
 
diff --git a/models/res_users.py b/models/res_users.py
index 0bd3044..7d12c2c 100644
--- a/models/res_users.py
+++ b/models/res_users.py
@@ -7,6 +7,7 @@ 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__)
 
@@ -45,10 +46,17 @@ class ResUser(models.Model):
                 self.is_bm_connection_ok = False
                 if force:
                     raise UserError(
-                        _("Something went wrong during connection : [%s]", e)
+                        _(
+                            "Something went wrong during connection : [%s]",
+                            exception_to_unicode(e),
+                        )
                     )
             return False
 
+    def force_auth(self):
+        # TODO : add notification that it works or not !
+        return self.bluemind_auth(force=True)
+
     @api.depends("company_id.bluemind_domain", "bluemind_login", "bluemind_password")
     def _compute_bluemind_user_id(self):
         for user in self:
@@ -70,7 +78,7 @@ class ResUser(models.Model):
             if user.bluemind_user_id:
                 user.bluemind_calendar_id = "calendar:Default:" + user.bluemind_user_id
 
-    def bluemind_sync(self):
+    def sync_bluemind_calendar(self):
         self.ensure_one()
         # TODO: add checks and error handling
         # Retrieve all events modified since last sync (followed by self.bluemind_id_version)
@@ -115,3 +123,18 @@ class ResUser(models.Model):
         odoo_events_to_delete.unlink(from_bluemind=True)
 
         self.last_sync_version = bm_last_version
+
+    @api.model
+    def _sync_all_bm_calendar(self):
+        """ Cron job """
+        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).sudo().sync_bluemind_calendar()
+            except Exception as e:
+                _logger.exception(
+                    "[%s] Calendar Synchro - Exception : %s !",
+                    user,
+                    exception_to_unicode(e),
+                )
diff --git a/views/res_users_views.xml b/views/res_users_views.xml
index 0c12d17..d577483 100644
--- a/views/res_users_views.xml
+++ b/views/res_users_views.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0" encoding="UTF-8" ?>
 <!-- Copyright 2021 Le Filament
      License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
 <odoo>
@@ -6,16 +6,32 @@
         <record id="view_users_form" model="ir.ui.view">
             <field name="name">res.users.form</field>
             <field name="model">res.users</field>
-            <field name="inherit_id" ref="base.view_users_form"/>
+            <field name="inherit_id" ref="base.view_users_form_simple_modif" />
             <field name="arch" type="xml">
-                <notebook colspan="4" position="inside">
+                <notebook position="inside">
                     <page string="Bluemind Calendar" name="bm_calendar">
                         <group class="text-break">
-                            <field name="bluemind_login"/>
-                            <field name="bluemind_password" password="True"/>
-                            <field name="bluemind_user_id" readonly="True"/>
-                            <field name="bluemind_calendar_id" readonly="True"/>
+                            <field name="bluemind_login" />
+                            <field name="bluemind_password" password="True" />
+                            <field name="bluemind_user_id" readonly="True" />
+                            <field name="bluemind_calendar_id" readonly="True" />
+                            <field name="is_bm_connection_ok" readonly="True" />
                         </group>
+                        <button
+                            string="Force authentication"
+                            class="btn btn-secondary"
+                            type="object"
+                            name="force_auth"
+                            context="{'default_user_id': id}"
+                        />
+                        <button
+                            string="Sync calendar"
+                            class="btn btn-primary"
+                            type="object"
+                            name="sync_bluemind_calendar"
+                            attrs="{'invisible': [('is_bm_connection_ok', '=', False)]}"
+                            context="{'default_user_id': id}"
+                        />
                     </page>
                 </notebook>
             </field>
-- 
GitLab