diff --git a/addons/google_calendar/__init__.py b/addons/google_calendar/__init__.py index addb295d7c1..ac0011716f6 100644 --- a/addons/google_calendar/__init__.py +++ b/addons/google_calendar/__init__.py @@ -1,3 +1,5 @@ -import res_config -import google_calendar +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + import controllers +import models diff --git a/addons/google_calendar/__openerp__.py b/addons/google_calendar/__openerp__.py index 4e6d5e9d593..3a722b9d923 100644 --- a/addons/google_calendar/__openerp__.py +++ b/addons/google_calendar/__openerp__.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - { 'name': 'Google Calendar', 'version': '1.0', @@ -14,11 +13,12 @@ The module adds the possibility to synchronize Google Calendar with Odoo 'depends': ['google_account', 'calendar'], 'qweb': ['static/src/xml/*.xml'], 'data': [ - 'res_config_view.xml', + 'data/google_calendar_data.xml', + 'data/google_calendar_data.xml', 'security/ir.model.access.csv', - 'views/google_calendar.xml', - 'views/res_users.xml', - 'google_calendar.xml', + 'views/res_config_views.xml', + 'views/res_users_views.xml', + 'views/google_calendar_templates.xml', ], 'demo': [], 'installable': True, diff --git a/addons/google_calendar/controllers/__init__.py b/addons/google_calendar/controllers/__init__.py index 8ee9bae18d9..f48716d3a69 100644 --- a/addons/google_calendar/controllers/__init__.py +++ b/addons/google_calendar/controllers/__init__.py @@ -1 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + import main diff --git a/addons/google_calendar/controllers/main.py b/addons/google_calendar/controllers/main.py index 233126bee64..dda2104e9bd 100644 --- a/addons/google_calendar/controllers/main.py +++ b/addons/google_calendar/controllers/main.py @@ -1,59 +1,58 @@ -import openerp.http as http -from openerp.http import request +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.http import request -class google_calendar_controller(http.Controller): +class GoogleCalendarController(http.Controller): @http.route('/google_calendar/sync_data', type='json', auth='user') def sync_data(self, arch, fields, model, **kw): - """ - This route/function is called when we want to synchronize openERP calendar with Google Calendar + """ This route/function is called when we want to synchronize openERP calendar with Google Calendar Function return a dictionary with the status : need_config_from_admin, need_auth, need_refresh, success if not calendar_event The dictionary may contains an url, to allow OpenERP Client to redirect user on this URL for authorization for example """ - if model == 'calendar.event': - gs_obj = request.registry['google.service'] - gc_obj = request.registry['google.calendar'] + GoogleService = request.env['google.service'] + GoogleCal = request.env['google.calendar'] # Checking that admin have already configured Google API for google synchronization ! - client_id = gs_obj.get_client_id(request.cr, request.uid, 'calendar', context=kw.get('local_context')) + context = kw.get('local_context', {}) + client_id = GoogleService.with_context(context).get_client_id('calendar') if not client_id or client_id == '': - action = '' - if gc_obj.can_authorize_google(request.cr, request.uid): - dummy, action = request.registry.get('ir.model.data').get_object_reference(request.cr, request.uid, - 'google_calendar', 'action_config_settings_google_calendar') - + action_id = '' + if GoogleCal.can_authorize_google(): + action_id = request.env.ref('google_calendar.action_config_settings_google_calendar').id return { "status": "need_config_from_admin", "url": '', - "action": action + "action": action_id } # Checking that user have already accepted OpenERP to access his calendar ! - if gc_obj.need_authorize(request.cr, request.uid, context=kw.get('local_context')): - url = gc_obj.authorize_google_uri(request.cr, request.uid, from_url=kw.get('fromurl'), context=kw.get('local_context')) + if GoogleCal.need_authorize(): + url = GoogleCal.with_context(context).authorize_google_uri(from_url=kw.get('fromurl')) return { "status": "need_auth", "url": url } # If App authorized, and user access accepted, We launch the synchronization - return gc_obj.synchronize_events(request.cr, request.uid, [], context=kw.get('local_context')) + return GoogleCal.with_context(context).synchronize_events() return {"status": "success"} @http.route('/google_calendar/remove_references', type='json', auth='user') def remove_references(self, model, **kw): - """ - This route/function is called when we want to remove all the references between one calendar OpenERP and one Google Calendar - """ + """ This route/function is called when we want to remove all the references between one calendar OpenERP and one Google Calendar """ status = "NOP" if model == 'calendar.event': - gc_obj = request.registry['google.calendar'] + GoogleCal = request.env['google.calendar'] # Checking that user have already accepted OpenERP to access his calendar ! - if gc_obj.remove_references(request.cr, request.uid, context=kw.get('local_context')): + context = kw.get('local_context', {}) + if GoogleCal.with_context(context).remove_references(): status = "OK" else: status = "KO" diff --git a/addons/google_calendar/google_calendar.xml b/addons/google_calendar/data/google_calendar_data.xml similarity index 96% rename from addons/google_calendar/google_calendar.xml rename to addons/google_calendar/data/google_calendar_data.xml index d2516f10233..3fc26e25351 100644 --- a/addons/google_calendar/google_calendar.xml +++ b/addons/google_calendar/data/google_calendar_data.xml @@ -1,7 +1,5 @@ - - - + Calendar synchronization @@ -14,6 +12,5 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py deleted file mode 100644 index 98181030f11..00000000000 --- a/addons/google_calendar/google_calendar.py +++ /dev/null @@ -1,1038 +0,0 @@ -# -*- coding: utf-8 -*- - -import operator -import json -import urllib2 - -import openerp -from openerp import tools -from openerp import SUPERUSER_ID -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, exception_to_unicode - -from openerp.tools.translate import _ -from openerp.http import request -from datetime import datetime, timedelta -from dateutil import parser -import pytz -from openerp.osv import fields, osv - -import logging -_logger = logging.getLogger(__name__) - - -def status_response(status): - return int(str(status)[0]) == 2 - - -class Meta(type): - """ This Meta class allow to define class as a structure, and so instancied variable - in __init__ to avoid to have side effect alike 'static' variable """ - def __new__(typ, name, parents, attrs): - methods = dict((k, v) for k, v in attrs.iteritems() - if callable(v)) - attrs = dict((k, v) for k, v in attrs.iteritems() - if not callable(v)) - - def init(self, **kw): - for k, v in attrs.iteritems(): - setattr(self, k, v) - for k, v in kw.iteritems(): - assert k in attrs - setattr(self, k, v) - - methods['__init__'] = init - methods['__getitem__'] = getattr - return type.__new__(typ, name, parents, methods) - - -class Struct(object): - __metaclass__ = Meta - - -class OpenerpEvent(Struct): - event = False - found = False - event_id = False - isRecurrence = False - isInstance = False - update = False - status = False - attendee_id = False - synchro = False - - -class GmailEvent(Struct): - event = False - found = False - isRecurrence = False - isInstance = False - update = False - status = False - - -class SyncEvent(object): - def __init__(self): - self.OE = OpenerpEvent() - self.GG = GmailEvent() - self.OP = None - - def __getitem__(self, key): - return getattr(self, key) - - def compute_OP(self, modeFull=True): - #If event are already in Gmail and in OpenERP - if self.OE.found and self.GG.found: - is_owner = self.OE.event.env.user.id == self.OE.event.user_id.id - #If the event has been deleted from one side, we delete on other side ! - if self.OE.status != self.GG.status and is_owner: - self.OP = Delete((self.OE.status and "OE") or (self.GG.status and "GG"), - 'The event has been deleted from one side, we delete on other side !') - #If event is not deleted ! - elif self.OE.status and (self.GG.status or not is_owner): - if self.OE.update.split('.')[0] != self.GG.update.split('.')[0]: - if self.OE.update < self.GG.update: - tmpSrc = 'GG' - elif self.OE.update > self.GG.update: - tmpSrc = 'OE' - assert tmpSrc in ['GG', 'OE'] - - if self[tmpSrc].isRecurrence: - if self[tmpSrc].status: - self.OP = Update(tmpSrc, 'Only need to update, because i\'m active') - else: - self.OP = Exclude(tmpSrc, 'Need to Exclude (Me = First event from recurrence) from recurrence') - - elif self[tmpSrc].isInstance: - self.OP = Update(tmpSrc, 'Only need to update, because already an exclu') - else: - self.OP = Update(tmpSrc, 'Simply Update... I\'m a single event') - else: - if not self.OE.synchro or self.OE.synchro.split('.')[0] < self.OE.update.split('.')[0]: - self.OP = Update('OE', 'Event already updated by another user, but not synchro with my google calendar') - else: - self.OP = NothingToDo("", 'Not update needed') - else: - self.OP = NothingToDo("", "Both are already deleted") - - # New in openERP... Create on create_events of synchronize function - elif self.OE.found and not self.GG.found: - if self.OE.status: - self.OP = Delete('OE', 'Update or delete from GOOGLE') - else: - if not modeFull: - self.OP = Delete('GG', 'Deleted from Odoo, need to delete it from Gmail if already created') - else: - self.OP = NothingToDo("", "Already Deleted in gmail and unlinked in Odoo") - elif self.GG.found and not self.OE.found: - tmpSrc = 'GG' - if not self.GG.status and not self.GG.isInstance: - # don't need to make something... because event has been created and deleted before the synchronization - self.OP = NothingToDo("", 'Nothing to do... Create and Delete directly') - else: - if self.GG.isInstance: - if self[tmpSrc].status: - self.OP = Exclude(tmpSrc, 'Need to create the new exclu') - else: - self.OP = Exclude(tmpSrc, 'Need to copy and Exclude') - else: - self.OP = Create(tmpSrc, 'New EVENT CREATE from GMAIL') - - def __str__(self): - return self.__repr__() - - def __repr__(self): - myPrint = "\n\n---- A SYNC EVENT ---" - myPrint += "\n ID OE: %s " % (self.OE.event and self.OE.event.id) - myPrint += "\n ID GG: %s " % (self.GG.event and self.GG.event.get('id', False)) - myPrint += "\n Name OE: %s " % (self.OE.event and self.OE.event.name.encode('utf8')) - myPrint += "\n Name GG: %s " % (self.GG.event and self.GG.event.get('summary', '').encode('utf8')) - myPrint += "\n Found OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found) - myPrint += "\n Recurrence OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence) - myPrint += "\n Instance OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance) - myPrint += "\n Synchro OE: %10s " % (self.OE.synchro) - myPrint += "\n Update OE: %10s " % (self.OE.update) - myPrint += "\n Update GG: %10s " % (self.GG.update) - myPrint += "\n Status OE:%5s vs GG: %5s" % (self.OE.status, self.GG.status) - if (self.OP is None): - myPrint += "\n Action %s" % "---!!!---NONE---!!!---" - else: - myPrint += "\n Action %s" % type(self.OP).__name__ - myPrint += "\n Source %s" % (self.OP.src) - myPrint += "\n comment %s" % (self.OP.info) - return myPrint - - -class SyncOperation(object): - def __init__(self, src, info, **kw): - self.src = src - self.info = info - for k, v in kw.items(): - setattr(self, k, v) - - def __str__(self): - return 'in__STR__' - - -class Create(SyncOperation): - pass - - -class Update(SyncOperation): - pass - - -class Delete(SyncOperation): - pass - - -class NothingToDo(SyncOperation): - pass - - -class Exclude(SyncOperation): - pass - - -class google_calendar(osv.AbstractModel): - STR_SERVICE = 'calendar' - _name = 'google.%s' % STR_SERVICE - - def generate_data(self, cr, uid, event, isCreating=False, context=None): - if not context: - context = {} - if event.allday: - start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.start, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T').split('T')[0] - final_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.stop, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=1), context=context).isoformat('T').split('T')[0] - type = 'date' - vstype = 'dateTime' - else: - start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.start, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') - final_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.stop, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') - type = 'dateTime' - vstype = 'date' - attendee_list = [] - for attendee in event.attendee_ids: - email = tools.email_split(attendee.email) - email = email[0] if email else 'NoEmail@mail.com' - attendee_list.append({ - 'email': email, - 'displayName': attendee.partner_id.name, - 'responseStatus': attendee.state or 'needsAction', - }) - - reminders = [] - for alarm in event.alarm_ids: - reminders.append({ - "method": "email" if alarm.type == "email" else "popup", - "minutes": alarm.duration_minutes - }) - data = { - "summary": event.name or '', - "description": event.description or '', - "start": { - type: start_date, - vstype: None, - 'timeZone': context.get('tz') or 'UTC', - }, - "end": { - type: final_date, - vstype: None, - 'timeZone': context.get('tz') or 'UTC', - }, - "attendees": attendee_list, - "reminders": { - "overrides": reminders, - "useDefault": "false" - }, - "location": event.location or '', - "visibility": event['privacy'] or 'public', - } - if event.recurrency and event.rrule: - data["recurrence"] = ["RRULE:" + event.rrule] - - if not event.active: - data["state"] = "cancelled" - - if not self.get_need_synchro_attendee(cr, uid, context=context): - data.pop("attendees") - if isCreating: - other_google_ids = [other_att.google_internal_event_id for other_att in event.attendee_ids if other_att.google_internal_event_id] - if other_google_ids: - data["id"] = other_google_ids[0] - return data - - def create_an_event(self, cr, uid, event, context=None): - gs_pool = self.pool['google.service'] - data = self.generate_data(cr, uid, event, isCreating=True, context=context) - - url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urllib2.quote('id,updated'), self.get_token(cr, uid, context)) - headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} - data_json = json.dumps(data) - - return gs_pool._do_request(cr, uid, url, data_json, headers, type='POST', context=context) - - def delete_an_event(self, cr, uid, event_id, context=None): - gs_pool = self.pool['google.service'] - - params = { - 'access_token': self.get_token(cr, uid, context) - } - headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} - url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event_id) - - return gs_pool._do_request(cr, uid, url, params, headers, type='DELETE', context=context) - - def get_calendar_primary_id(self, cr, uid, context=None): - params = { - 'fields': 'id', - 'access_token': self.get_token(cr, uid, context) - } - headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} - - url = "/calendar/v3/calendars/primary" - - try: - st, content, ask_time = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context) - except urllib2.HTTPError, e: - if e.code == 401: # Token invalid / Acces unauthorized - error_msg = _("Your token is invalid or has been revoked !") - - registry = openerp.modules.registry.RegistryManager.get(request.session.db) - with registry.cursor() as cur: - self.pool['res.users'].write(cur, SUPERUSER_ID, [uid], {'google_calendar_token': False, 'google_calendar_token_validity': False}, context=context) - - raise self.pool.get('res.config.settings').get_config_warning(cr, error_msg, context=context) - raise - - return (status_response(st), content['id'] or False, ask_time) - - def get_event_synchro_dict(self, cr, uid, lastSync=False, token=False, nextPageToken=False, context=None): - if not token: - token = self.get_token(cr, uid, context) - - params = { - 'fields': 'items,nextPageToken', - 'access_token': token, - 'maxResults': 1000, - } - - if lastSync: - params['updatedMin'] = lastSync.strftime("%Y-%m-%dT%H:%M:%S.%fz") - params['showDeleted'] = True - else: - params['timeMin'] = self.get_minTime(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz") - - headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} - - url = "/calendar/v3/calendars/%s/events" % 'primary' - if nextPageToken: - params['pageToken'] = nextPageToken - - status, content, ask_time = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context) - - google_events_dict = {} - for google_event in content['items']: - google_events_dict[google_event['id']] = google_event - - if content.get('nextPageToken'): - google_events_dict.update( - self.get_event_synchro_dict(cr, uid, lastSync=lastSync, token=token, nextPageToken=content['nextPageToken'], context=context) - ) - - return google_events_dict - - def get_one_event_synchro(self, cr, uid, google_id, context=None): - token = self.get_token(cr, uid, context) - - params = { - 'access_token': token, - 'maxResults': 1000, - 'showDeleted': True, - } - - headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} - - url = "/calendar/v3/calendars/%s/events/%s" % ('primary', google_id) - try: - status, content, ask_time = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context) - except Exception, e: - _logger.info("Calendar Synchro - In except of get_one_event_synchro") - _logger.info(exception_to_unicode(e)) - return False - - return status_response(status) and content or False - - def update_to_google(self, cr, uid, oe_event, google_event, context): - calendar_event = self.pool['calendar.event'] - - url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'], 'id,updated', self.get_token(cr, uid, context)) - headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} - data = self.generate_data(cr, uid, oe_event, context=context) - data['sequence'] = google_event.get('sequence', 0) - data_json = json.dumps(data) - - status, content, ask_time = self.pool['google.service']._do_request(cr, uid, url, data_json, headers, type='PATCH', context=context) - - update_date = datetime.strptime(content['updated'], "%Y-%m-%dT%H:%M:%S.%fz") - calendar_event.write(cr, uid, [oe_event.id], {'oe_update_date': update_date}) - - if context['curr_attendee']: - self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date}, context) - - def update_an_event(self, cr, uid, event, context=None): - data = self.generate_data(cr, uid, event, context=context) - - url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event.google_internal_event_id) - headers = {} - data['access_token'] = self.get_token(cr, uid, context) - - status, response, ask_time = self.pool['google.service']._do_request(cr, uid, url, data, headers, type='GET', context=context) - #TO_CHECK : , if http fail, no event, do DELETE ? - return response - - def update_recurrent_event_exclu(self, cr, uid, instance_id, event_ori_google_id, event_new, context=None): - gs_pool = self.pool['google.service'] - - data = self.generate_data(cr, uid, event_new, context=context) - - data['recurringEventId'] = event_ori_google_id - data['originalStartTime'] = event_new.recurrent_id_date - - url = "/calendar/v3/calendars/%s/events/%s?access_token=%s" % ('primary', instance_id, self.get_token(cr, uid, context)) - headers = {'Content-type': 'application/json'} - - data['sequence'] = self.get_sequence(cr, uid, instance_id, context) - - data_json = json.dumps(data) - return gs_pool._do_request(cr, uid, url, data_json, headers, type='PUT', context=context) - - def create_from_google(self, cr, uid, event, partner_id, context=None): - context_tmp = dict(context, NewMeeting=True) - res = self.update_from_google(cr, uid, False, event.GG.event, "create", context=context_tmp) - event.OE.event_id = res - meeting = self.pool['calendar.event'].browse(cr, uid, res, context=context) - attendee_record_id = self.pool['calendar.attendee'].search(cr, uid, [('partner_id', '=', partner_id), ('event_id', '=', res)], context=context) - self.pool['calendar.attendee'].write(cr, uid, attendee_record_id, {'oe_synchro_date': meeting.oe_update_date, 'google_internal_event_id': event.GG.event['id']}, context=context_tmp) - if meeting.recurrency: - attendee_ids = self.pool['calendar.attendee'].search(cr, SUPERUSER_ID, [('google_internal_event_id', '=ilike', '%s\_%%' % event.GG.event['id'])], context=context) - attendees = self.pool['calendar.attendee'].browse(cr, SUPERUSER_ID, attendee_ids, context=context) - excluded_recurrent_event_ids = set(attendee.event_id for attendee in attendees) - for event in excluded_recurrent_event_ids: - event.write({'recurrent_id': meeting.id, 'recurrent_id_date': event.start, 'user_id': meeting.user_id.id}) - return event - - def update_from_google(self, cr, uid, event, single_event_dict, type, context): - if context is None: - context = [] - - calendar_event = self.pool['calendar.event'] - res_partner_obj = self.pool['res.partner'] - calendar_attendee_obj = self.pool['calendar.attendee'] - calendar_alarm_obj = self.pool['calendar.alarm'] - user_obj = self.pool['res.users'] - myPartnerID = user_obj.browse(cr, uid, uid, context).partner_id.id - attendee_record = [] - alarm_record = set() - partner_record = [(4, myPartnerID)] - result = {} - - if self.get_need_synchro_attendee(cr, uid, context=context): - for google_attendee in single_event_dict.get('attendees', []): - partner_email = google_attendee.get('email', False) - if type == "write": - for oe_attendee in event['attendee_ids']: - if oe_attendee.email == partner_email: - calendar_attendee_obj.write(cr, uid, [oe_attendee.id], {'state': google_attendee['responseStatus']}, context=context) - google_attendee['found'] = True - continue - - if google_attendee.get('found'): - continue - - attendee_id = res_partner_obj.search(cr, uid, [('email', '=', partner_email)], context=context) - if not attendee_id: - data = { - 'email': partner_email, - 'customer': False, - 'name': google_attendee.get("displayName", False) or partner_email - } - attendee_id = [res_partner_obj.create(cr, uid, data, context=context)] - attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context) - partner_record.append((4, attendee.get('id'))) - attendee['partner_id'] = attendee.pop('id') - attendee['state'] = google_attendee['responseStatus'] - attendee_record.append((0, 0, attendee)) - for google_alarm in single_event_dict.get('reminders', {}).get('overrides', []): - alarm_id = calendar_alarm_obj.search( - cr, - uid, - [ - ('type', '=', google_alarm['method'] if google_alarm['method'] == 'email' else 'notification'), - ('duration_minutes', '=', google_alarm['minutes']) - ], - context=context - ) - if not alarm_id: - data = { - 'type': google_alarm['method'] if google_alarm['method'] == 'email' else 'notification', - 'duration': google_alarm['minutes'], - 'interval': 'minutes', - 'name': "%s minutes - %s" % (google_alarm['minutes'], google_alarm['method']) - } - alarm_id = [calendar_alarm_obj.create(cr, uid, data, context=context)] - alarm_record.add(alarm_id[0]) - - UTC = pytz.timezone('UTC') - if single_event_dict.get('start') and single_event_dict.get('end'): # If not cancelled - - if single_event_dict['start'].get('dateTime', False) and single_event_dict['end'].get('dateTime', False): - date = parser.parse(single_event_dict['start']['dateTime']) - stop = parser.parse(single_event_dict['end']['dateTime']) - date = str(date.astimezone(UTC))[:-6] - stop = str(stop.astimezone(UTC))[:-6] - allday = False - else: - date = (single_event_dict['start']['date']) - stop = (single_event_dict['end']['date']) - d_end = datetime.strptime(stop, DEFAULT_SERVER_DATE_FORMAT) - allday = True - d_end = d_end + timedelta(days=-1) - stop = d_end.strftime(DEFAULT_SERVER_DATE_FORMAT) - - update_date = datetime.strptime(single_event_dict['updated'], "%Y-%m-%dT%H:%M:%S.%fz") - result.update({ - 'start': date, - 'stop': stop, - 'allday': allday - }) - result.update({ - 'attendee_ids': attendee_record, - 'partner_ids': list(set(partner_record)), - 'alarm_ids': [(6, 0, list(alarm_record))], - - 'name': single_event_dict.get('summary', 'Event'), - 'description': single_event_dict.get('description', False), - 'location': single_event_dict.get('location', False), - 'class': single_event_dict.get('visibility', 'public'), - 'oe_update_date': update_date, - }) - - if single_event_dict.get("recurrence", False): - rrule = [rule for rule in single_event_dict["recurrence"] if rule.startswith("RRULE:")][0][6:] - result['rrule'] = rrule - - context = dict(context or {}, no_mail_to_attendees=True) - if type == "write": - res = calendar_event.write(cr, uid, event['id'], result, context=context) - elif type == "copy": - result['recurrence'] = True - res = calendar_event.write(cr, uid, [event['id']], result, context=context) - elif type == "create": - res = calendar_event.create(cr, uid, result, context=context) - - if context['curr_attendee']: - self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date, 'google_internal_event_id': single_event_dict.get('id', False)}, context) - return res - - def remove_references(self, cr, uid, context=None): - current_user = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context) - reset_data = { - 'google_calendar_rtoken': False, - 'google_calendar_token': False, - 'google_calendar_token_validity': False, - 'google_calendar_last_sync_date': False, - 'google_calendar_cal_id': False, - } - - all_my_attendees = self.pool['calendar.attendee'].search(cr, uid, [('partner_id', '=', current_user.partner_id.id)], context=context) - self.pool['calendar.attendee'].write(cr, uid, all_my_attendees, {'oe_synchro_date': False, 'google_internal_event_id': False}, context=context) - current_user.write(reset_data) - return True - - def synchronize_events_cron(self, cr, uid, context=None): - ids = self.pool['res.users'].search(cr, uid, [('google_calendar_last_sync_date', '!=', False)], context=context) - _logger.info("Calendar Synchro - Started by cron") - - for user_to_sync in ids: - _logger.info("Calendar Synchro - Starting synchronization for a new user [%s] " % user_to_sync) - try: - resp = self.synchronize_events(cr, user_to_sync, False, lastSync=True, context=None) - if resp.get("status") == "need_reset": - _logger.info("[%s] Calendar Synchro - Failed - NEED RESET !" % user_to_sync) - else: - _logger.info("[%s] Calendar Synchro - Done with status : %s !" % (user_to_sync, resp.get("status"))) - except Exception, e: - _logger.info("[%s] Calendar Synchro - Exception : %s !" % (user_to_sync, exception_to_unicode(e))) - _logger.info("Calendar Synchro - Ended by cron") - - def synchronize_events(self, cr, uid, ids, lastSync=True, context=None): - if context is None: - context = {} - - user_to_sync = ids and ids[0] or uid - current_user = self.pool['res.users'].browse(cr, SUPERUSER_ID, user_to_sync, context=context) - - st, current_google, ask_time = self.get_calendar_primary_id(cr, user_to_sync, context=context) - - if current_user.google_calendar_cal_id: - if current_google != current_user.google_calendar_cal_id: - return { - "status": "need_reset", - "info": { - "old_name": current_user.google_calendar_cal_id, - "new_name": current_google - }, - "url": '' - } - - if lastSync and self.get_last_sync_date(cr, user_to_sync, context=context) and not self.get_disable_since_synchro(cr, user_to_sync, context=context): - lastSync = self.get_last_sync_date(cr, user_to_sync, context) - _logger.info("[%s] Calendar Synchro - MODE SINCE_MODIFIED : %s !" % (user_to_sync, lastSync.strftime(DEFAULT_SERVER_DATETIME_FORMAT))) - else: - lastSync = False - _logger.info("[%s] Calendar Synchro - MODE FULL SYNCHRO FORCED" % user_to_sync) - else: - current_user.write({'google_calendar_cal_id': current_google}) - lastSync = False - _logger.info("[%s] Calendar Synchro - MODE FULL SYNCHRO - NEW CAL ID" % user_to_sync) - - new_ids = [] - new_ids += self.create_new_events(cr, user_to_sync, context=context) - new_ids += self.bind_recurring_events_to_google(cr, user_to_sync, context) - - res = self.update_events(cr, user_to_sync, lastSync, context) - - current_user.write({'google_calendar_last_sync_date': ask_time}) - return { - "status": res and "need_refresh" or "no_new_event_from_google", - "url": '' - } - - def create_new_events(self, cr, uid, context=None): - if context is None: - context = {} - - new_ids = [] - ev_obj = self.pool['calendar.event'] - att_obj = self.pool['calendar.attendee'] - user_obj = self.pool['res.users'] - myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id - - context_norecurrent = context.copy() - context_norecurrent['virtual_id'] = False - my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), - ('google_internal_event_id', '=', False), - '|', - ('event_id.stop', '>', self.get_minTime(cr, uid, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), - ('event_id.final_date', '>', self.get_minTime(cr, uid, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), - ], context=context_norecurrent) - for att in att_obj.browse(cr, uid, my_att_ids, context=context): - other_google_ids = [other_att.google_internal_event_id for other_att in att.event_id.attendee_ids if other_att.google_internal_event_id and other_att.id != att.id] - for other_google_id in other_google_ids: - if self.get_one_event_synchro(cr, uid, other_google_id, context=context): - att_obj.write(cr, uid, [att.id], {'google_internal_event_id': other_google_id}) - break - else: - if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0: - st, response, ask_time = self.create_an_event(cr, uid, att.event_id, context=context) - if status_response(st): - update_date = datetime.strptime(response['updated'], "%Y-%m-%dT%H:%M:%S.%fz") - ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date': update_date}) - new_ids.append(response['id']) - att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date': update_date}) - cr.commit() - else: - _logger.warning("Impossible to create event %s. [%s] Enable DEBUG for response detail.", att.event_id.id, st) - _logger.debug("Response : %s" % response) - return new_ids - - def get_context_no_virtual(self, context): - context_norecurrent = context.copy() - context_norecurrent['virtual_id'] = False - context_norecurrent['active_test'] = False - return context_norecurrent - - def bind_recurring_events_to_google(self, cr, uid, context=None): - if context is None: - context = {} - - new_ids = [] - ev_obj = self.pool['calendar.event'] - att_obj = self.pool['calendar.attendee'] - user_obj = self.pool['res.users'] - myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id - - context_norecurrent = self.get_context_no_virtual(context) - my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('google_internal_event_id', '=', False)], context=context_norecurrent) - - for att in att_obj.browse(cr, uid, my_att_ids, context=context): - if att.event_id.recurrent_id and att.event_id.recurrent_id > 0: - new_google_internal_event_id = False - source_event_record = ev_obj.browse(cr, uid, att.event_id.recurrent_id, context) - source_attendee_record_id = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('event_id', '=', source_event_record.id)], context=context) - if not source_attendee_record_id: - continue - source_attendee_record = att_obj.browse(cr, uid, source_attendee_record_id, context)[0] - - if att.event_id.recurrent_id_date and source_event_record.allday and source_attendee_record.google_internal_event_id: - new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.split(' ')[0].replace('-', '') - elif att.event_id.recurrent_id_date and source_attendee_record.google_internal_event_id: - new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.replace('-', '').replace(' ', 'T').replace(':', '') + 'Z' - - if new_google_internal_event_id: - #TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE ! - try: - st, response, ask_time = self.update_recurrent_event_exclu(cr, uid, new_google_internal_event_id, source_attendee_record.google_internal_event_id, att.event_id, context=context) - if status_response(st): - att_obj.write(cr, uid, [att.id], {'google_internal_event_id': new_google_internal_event_id}, context=context) - new_ids.append(new_google_internal_event_id) - cr.commit() - else: - _logger.warning("Impossible to create event %s. [%s]" % (att.event_id.id, st)) - _logger.debug("Response : %s" % response) - except: - pass - return new_ids - - def update_events(self, cr, uid, lastSync=False, context=None): - context = dict(context or {}) - - calendar_event = self.pool['calendar.event'] - user_obj = self.pool['res.users'] - att_obj = self.pool['calendar.attendee'] - myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id - context_novirtual = self.get_context_no_virtual(context) - - if lastSync: - try: - all_event_from_google = self.get_event_synchro_dict(cr, uid, lastSync=lastSync, context=context) - except urllib2.HTTPError, e: - if e.code == 410: # GONE, Google is lost. - # we need to force the rollback from this cursor, because it locks my res_users but I need to write in this tuple before to raise. - cr.rollback() - registry = openerp.modules.registry.RegistryManager.get(request.session.db) - with registry.cursor() as cur: - self.pool['res.users'].write(cur, SUPERUSER_ID, [uid], {'google_calendar_last_sync_date': False}, context=context) - error_key = json.loads(str(e)) - error_key = error_key.get('error', {}).get('message', 'nc') - error_msg = _("Google is lost... the next synchro will be a full synchro. \n\n %s") % error_key - raise self.pool.get('res.config.settings').get_config_warning(cr, error_msg, context=context) - - my_google_att_ids = att_obj.search(cr, uid, [ - ('partner_id', '=', myPartnerID), - ('google_internal_event_id', 'in', all_event_from_google.keys()) - ], context=context_novirtual) - - my_openerp_att_ids = att_obj.search(cr, uid, [ - ('partner_id', '=', myPartnerID), - ('event_id.oe_update_date', '>', lastSync and lastSync.strftime(DEFAULT_SERVER_DATETIME_FORMAT) or self.get_minTime(cr, uid, context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), - ('google_internal_event_id', '!=', False), - ], context=context_novirtual) - - my_openerp_googleinternal_ids = att_obj.read(cr, uid, my_openerp_att_ids, ['google_internal_event_id', 'event_id'], context=context_novirtual) - - if self.get_print_log(cr, uid, context=context): - _logger.info("Calendar Synchro - \n\nUPDATE IN GOOGLE\n%s\n\nRETRIEVE FROM OE\n%s\n\nUPDATE IN OE\n%s\n\nRETRIEVE FROM GG\n%s\n\n" % (all_event_from_google, my_google_att_ids, my_openerp_att_ids, my_openerp_googleinternal_ids)) - - for giid in my_openerp_googleinternal_ids: - active = True # if not sure, we request google - if giid.get('event_id'): - active = calendar_event.browse(cr, uid, int(giid.get('event_id')[0]), context=context_novirtual).active - - if giid.get('google_internal_event_id') and not all_event_from_google.get(giid.get('google_internal_event_id')) and active: - one_event = self.get_one_event_synchro(cr, uid, giid.get('google_internal_event_id'), context=context) - if one_event: - all_event_from_google[one_event['id']] = one_event - - my_att_ids = list(set(my_google_att_ids + my_openerp_att_ids)) - - else: - domain = [ - ('partner_id', '=', myPartnerID), - ('google_internal_event_id', '!=', False), - '|', - ('event_id.stop', '>', self.get_minTime(cr, uid, context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), - ('event_id.final_date', '>', self.get_minTime(cr, uid, context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), - ] - - # Select all events from OpenERP which have been already synchronized in gmail - my_att_ids = att_obj.search(cr, uid, domain, context=context_novirtual) - all_event_from_google = self.get_event_synchro_dict(cr, uid, lastSync=False, context=context) - - event_to_synchronize = {} - for att in att_obj.browse(cr, uid, my_att_ids, context=context): - event = att.event_id - - base_event_id = att.google_internal_event_id.rsplit('_', 1)[0] - - if base_event_id not in event_to_synchronize: - event_to_synchronize[base_event_id] = {} - - if att.google_internal_event_id not in event_to_synchronize[base_event_id]: - event_to_synchronize[base_event_id][att.google_internal_event_id] = SyncEvent() - - ev_to_sync = event_to_synchronize[base_event_id][att.google_internal_event_id] - - ev_to_sync.OE.attendee_id = att.id - ev_to_sync.OE.event = event - ev_to_sync.OE.found = True - ev_to_sync.OE.event_id = event.id - ev_to_sync.OE.isRecurrence = event.recurrency - ev_to_sync.OE.isInstance = bool(event.recurrent_id and event.recurrent_id > 0) - ev_to_sync.OE.update = event.oe_update_date - ev_to_sync.OE.status = event.active - ev_to_sync.OE.synchro = att.oe_synchro_date - - for event in all_event_from_google.values(): - event_id = event.get('id') - base_event_id = event_id.rsplit('_', 1)[0] - - if base_event_id not in event_to_synchronize: - event_to_synchronize[base_event_id] = {} - - if event_id not in event_to_synchronize[base_event_id]: - event_to_synchronize[base_event_id][event_id] = SyncEvent() - - ev_to_sync = event_to_synchronize[base_event_id][event_id] - - ev_to_sync.GG.event = event - ev_to_sync.GG.found = True - ev_to_sync.GG.isRecurrence = bool(event.get('recurrence', '')) - ev_to_sync.GG.isInstance = bool(event.get('recurringEventId', 0)) - ev_to_sync.GG.update = event.get('updated', None) # if deleted, no date without browse event - if ev_to_sync.GG.update: - ev_to_sync.GG.update = ev_to_sync.GG.update.replace('T', ' ').replace('Z', '') - ev_to_sync.GG.status = (event.get('status') != 'cancelled') - - ###################### - # PRE-PROCESSING # - ###################### - for base_event in event_to_synchronize: - for current_event in event_to_synchronize[base_event]: - event_to_synchronize[base_event][current_event].compute_OP(modeFull=not lastSync) - if self.get_print_log(cr, uid, context=context): - if not isinstance(event_to_synchronize[base_event][current_event].OP, NothingToDo): - _logger.info(event_to_synchronize[base_event]) - - ###################### - # DO ACTION # - ###################### - for base_event in event_to_synchronize: - event_to_synchronize[base_event] = sorted(event_to_synchronize[base_event].iteritems(), key=operator.itemgetter(0)) - for current_event in event_to_synchronize[base_event]: - cr.commit() - event = current_event[1] # event is an Sync Event ! - actToDo = event.OP - actSrc = event.OP.src - - context['curr_attendee'] = event.OE.attendee_id - - if isinstance(actToDo, NothingToDo): - continue - elif isinstance(actToDo, Create): - if actSrc == 'GG': - self.create_from_google(cr, uid, event, myPartnerID, context=context) - elif actSrc == 'OE': - raise "Should be never here, creation for OE is done before update !" - #TODO Add to batch - elif isinstance(actToDo, Update): - if actSrc == 'GG': - self.update_from_google(cr, uid, event.OE.event, event.GG.event, 'write', context) - elif actSrc == 'OE': - self.update_to_google(cr, uid, event.OE.event, event.GG.event, context) - elif isinstance(actToDo, Exclude): - if actSrc == 'OE': - self.delete_an_event(cr, uid, current_event[0], context=context) - elif actSrc == 'GG': - new_google_event_id = event.GG.event['id'].rsplit('_', 1)[1] - if 'T' in new_google_event_id: - new_google_event_id = new_google_event_id.replace('T', '')[:-1] - else: - new_google_event_id = new_google_event_id + "000000" - - if event.GG.status: - parent_event = {} - if not event_to_synchronize[base_event][0][1].OE.event_id: - main_ev = att_obj.search_read(cr, uid, [('google_internal_event_id', '=', event.GG.event['id'].rsplit('_', 1)[0])], fields=['event_id'], context=context_novirtual) - event_to_synchronize[base_event][0][1].OE.event_id = main_ev[0].get('event_id')[0] if main_ev else None - if event_to_synchronize[base_event][0][1].OE.event_id: - parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id) - res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context) - else: - self.create_from_google(cr, uid, event, myPartnerID, context=context) - else: - parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id - if parent_oe_id: - calendar_event.unlink(cr, uid, "%s-%s" % (parent_oe_id, new_google_event_id), can_be_deleted=True, context=context) - - elif isinstance(actToDo, Delete): - if actSrc == 'GG': - try: - self.delete_an_event(cr, uid, current_event[0], context=context) - except urllib2.HTTPError, e: - # if already deleted from gmail or never created - if e.code in (404, 410,): - pass - else: - raise e - elif actSrc == 'OE': - calendar_event.unlink(cr, uid, event.OE.event_id, can_be_deleted=False, context=context) - return True - - def check_and_sync(self, cr, uid, oe_event, google_event, context): - if datetime.strptime(oe_event.oe_update_date, "%Y-%m-%d %H:%M:%S.%f") > datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"): - self.update_to_google(cr, uid, oe_event, google_event, context) - elif datetime.strptime(oe_event.oe_update_date, "%Y-%m-%d %H:%M:%S.%f") < datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"): - self.update_from_google(cr, uid, oe_event, google_event, 'write', context) - - def get_sequence(self, cr, uid, instance_id, context=None): - gs_pool = self.pool['google.service'] - - params = { - 'fields': 'sequence', - 'access_token': self.get_token(cr, uid, context) - } - - headers = {'Content-type': 'application/json'} - - url = "/calendar/v3/calendars/%s/events/%s" % ('primary', instance_id) - - st, content, ask_time = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context) - return content.get('sequence', 0) -################################# -## MANAGE CONNEXION TO GMAIL ## -################################# - - def get_token(self, cr, uid, context=None): - current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) - if not current_user.google_calendar_token_validity or \ - datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) < (datetime.now() + timedelta(minutes=1)): - self.do_refresh_token(cr, uid, context=context) - current_user.refresh() - return current_user.google_calendar_token - - def get_last_sync_date(self, cr, uid, context=None): - current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) - return current_user.google_calendar_last_sync_date and datetime.strptime(current_user.google_calendar_last_sync_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(minutes=0) or False - - def do_refresh_token(self, cr, uid, context=None): - current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) - gs_pool = self.pool['google.service'] - - all_token = gs_pool._refresh_google_token_json(cr, uid, current_user.google_calendar_rtoken, self.STR_SERVICE, context=context) - - vals = {} - vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in')) - vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') - - self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context) - - def need_authorize(self, cr, uid, context=None): - current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) - return current_user.google_calendar_rtoken is False - - def get_calendar_scope(self, RO=False): - readonly = '.readonly' if RO else '' - return 'https://www.googleapis.com/auth/calendar%s' % (readonly) - - def authorize_google_uri(self, cr, uid, from_url='http://www.openerp.com', context=None): - url = self.pool['google.service']._get_authorize_uri(cr, uid, from_url, self.STR_SERVICE, scope=self.get_calendar_scope(), context=context) - return url - - def can_authorize_google(self, cr, uid, context=None): - return self.pool['res.users']._is_admin(cr, uid, [uid]) - - def set_all_tokens(self, cr, uid, authorization_code, context=None): - gs_pool = self.pool['google.service'] - all_token = gs_pool._get_google_token_json(cr, uid, authorization_code, self.STR_SERVICE, context=context) - - vals = {} - vals['google_%s_rtoken' % self.STR_SERVICE] = all_token.get('refresh_token') - vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in')) - vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') - self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context) - - def get_minTime(self, cr, uid, context=None): - number_of_week = int(self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.week_synchro', default=13)) - return datetime.now() - timedelta(weeks=number_of_week) - - def get_need_synchro_attendee(self, cr, uid, context=None): - return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_synchro_attendee', default=True) - - def get_disable_since_synchro(self, cr, uid, context=None): - return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_since_synchro', default=False) - - def get_print_log(self, cr, uid, context=None): - return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.debug_print', default=False) - - -class res_users(osv.Model): - _inherit = 'res.users' - - _columns = { - 'google_calendar_rtoken': fields.char('Refresh Token', copy=False), - 'google_calendar_token': fields.char('User token', copy=False), - 'google_calendar_token_validity': fields.datetime('Token Validity', copy=False), - 'google_calendar_last_sync_date': fields.datetime('Last synchro date', copy=False), - 'google_calendar_cal_id': fields.char('Calendar ID', help='Last Calendar ID who has been synchronized. If it is changed, we remove \ -all links between GoogleID and Odoo Google Internal ID', copy=False) - } - - -class calendar_event(osv.Model): - _inherit = "calendar.event" - - def get_fields_need_update_google(self, cr, uid, context=None): - recurrent_fields = self._get_recurrent_fields(cr, uid, context=context) - return recurrent_fields + ['name', 'description', 'allday', 'start', 'date_end', 'stop', - 'attendee_ids', 'alarm_ids', 'location', 'class', 'active', - 'start_date', 'start_datetime', 'stop_date', 'stop_datetime'] - - def write(self, cr, uid, ids, vals, context=None): - if context is None: - context = {} - sync_fields = set(self.get_fields_need_update_google(cr, uid, context)) - if (set(vals.keys()) & sync_fields) and 'oe_update_date' not in vals.keys() and 'NewMeeting' not in context: - vals['oe_update_date'] = datetime.now() - - return super(calendar_event, self).write(cr, uid, ids, vals, context=context) - - def copy(self, cr, uid, id, default=None, context=None): - default = default or {} - if default.get('write_type', False): - del default['write_type'] - elif default.get('recurrent_id', False): - default['oe_update_date'] = datetime.now() - else: - default['oe_update_date'] = False - return super(calendar_event, self).copy(cr, uid, id, default, context) - - def unlink(self, cr, uid, ids, can_be_deleted=False, context=None): - return super(calendar_event, self).unlink(cr, uid, ids, can_be_deleted=can_be_deleted, context=context) - - _columns = { - 'oe_update_date': fields.datetime('Odoo Update Date'), - } - - -class calendar_attendee(osv.Model): - _inherit = 'calendar.attendee' - - _columns = { - 'google_internal_event_id': fields.char('Google Calendar Event Id'), - 'oe_synchro_date': fields.datetime('Odoo Synchro Date'), - } - _sql_constraints = [('google_id_uniq', 'unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!')] - - def write(self, cr, uid, ids, vals, context=None): - if context is None: - context = {} - - for id in ids: - ref = vals.get('event_id', self.browse(cr, uid, id, context=context).event_id.id) - - # If attendees are updated, we need to specify that next synchro need an action - # Except if it come from an update_from_google - if not context.get('curr_attendee', False) and not context.get('NewMeeting', False): - self.pool['calendar.event'].write(cr, uid, ref, {'oe_update_date': datetime.now()}, context) - return super(calendar_attendee, self).write(cr, uid, ids, vals, context=context) diff --git a/addons/google_calendar/models/__init__.py b/addons/google_calendar/models/__init__.py new file mode 100644 index 00000000000..898ce8a88e8 --- /dev/null +++ b/addons/google_calendar/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import calendar +import res_users +import google_calendar +import res_config diff --git a/addons/google_calendar/models/calendar.py b/addons/google_calendar/models/calendar.py new file mode 100644 index 00000000000..bb87f2f7e48 --- /dev/null +++ b/addons/google_calendar/models/calendar.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class Meeting(models.Model): + + _inherit = "calendar.event" + + oe_update_date = fields.Datetime('Odoo Update Date') + + @api.model + def get_fields_need_update_google(self): + recurrent_fields = self._get_recurrent_fields() + return recurrent_fields + ['name', 'description', 'allday', 'start', 'date_end', 'stop', + 'attendee_ids', 'alarm_ids', 'location', 'privacy', 'active', + 'start_date', 'start_datetime', 'stop_date', 'stop_datetime'] + + @api.multi + def write(self, values): + sync_fields = set(self.get_fields_need_update_google()) + if (set(values.keys()) and sync_fields) and 'oe_update_date' not in values.keys() and 'NewMeeting' not in self._context: + values['oe_update_date'] = fields.Datetime.now() + return super(Meeting, self).write(values) + + @api.multi + def copy(self, default=None): + default = default or {} + if default.get('write_type', False): + del default['write_type'] + elif default.get('recurrent_id', False): + default['oe_update_date'] = fields.Datetime.now() + else: + default['oe_update_date'] = False + return super(Meeting, self).copy(default) + + @api.multi + def unlink(self, can_be_deleted=False): + return super(Meeting, self).unlink(can_be_deleted=can_be_deleted) + + +class Attendee(models.Model): + + _inherit = 'calendar.attendee' + + google_internal_event_id = fields.Char('Google Calendar Event Id') + oe_synchro_date = fields.Datetime('Odoo Synchro Date') + + _sql_constraints = [ + ('google_id_uniq', 'unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!') + ] + + @api.multi + def write(self, values): + for attendee in self: + meeting_id_to_update = values.get('event_id', attendee.event_id.id) + + # If attendees are updated, we need to specify that next synchro need an action + # Except if it come from an update_from_google + if not self._context.get('curr_attendee', False) and not self._context.get('NewMeeting', False): + self.env['calendar.event'].browse(meeting_id_to_update).write({'oe_update_date': fields.Datetime.now()}) + return super(Attendee, self).write(values) diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py new file mode 100644 index 00000000000..9b50835e6cf --- /dev/null +++ b/addons/google_calendar/models/google_calendar.py @@ -0,0 +1,919 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, timedelta +from dateutil import parser +import json +import logging +import operator +import pytz +import urllib2 + +from odoo import api, fields, models, tools, _ +from odoo.tools import exception_to_unicode + +_logger = logging.getLogger(__name__) + + +def status_response(status): + return int(str(status)[0]) == 2 + + +class Meta(type): + """ This Meta class allow to define class as a structure, and so instancied variable + in __init__ to avoid to have side effect alike 'static' variable """ + def __new__(typ, name, parents, attrs): + methods = dict((k, v) for k, v in attrs.iteritems() + if callable(v)) + attrs = dict((k, v) for k, v in attrs.iteritems() + if not callable(v)) + + def init(self, **kw): + for key, val in attrs.iteritems(): + setattr(self, key, val) + for key, val in kw.iteritems(): + assert key in attrs + setattr(self, key, val) + + methods['__init__'] = init + methods['__getitem__'] = getattr + return type.__new__(typ, name, parents, methods) + + +class Struct(object): + __metaclass__ = Meta + + +class OdooEvent(Struct): + event = False + found = False + event_id = False + isRecurrence = False + isInstance = False + update = False + status = False + attendee_id = False + synchro = False + + +class GmailEvent(Struct): + event = False + found = False + isRecurrence = False + isInstance = False + update = False + status = False + + +class SyncEvent(object): + def __init__(self): + self.OE = OdooEvent() + self.GG = GmailEvent() + self.OP = None + + def __getitem__(self, key): + return getattr(self, key) + + def compute_OP(self, modeFull=True): + #If event are already in Gmail and in Odoo + if self.OE.found and self.GG.found: + is_owner = self.OE.event.env.user.id == self.OE.event.user_id.id + #If the event has been deleted from one side, we delete on other side ! + if self.OE.status != self.GG.status and is_owner: + self.OP = Delete((self.OE.status and "OE") or (self.GG.status and "GG"), + 'The event has been deleted from one side, we delete on other side !') + #If event is not deleted ! + elif self.OE.status and (self.GG.status or not is_owner): + if self.OE.update.split('.')[0] != self.GG.update.split('.')[0]: + if self.OE.update < self.GG.update: + tmpSrc = 'GG' + elif self.OE.update > self.GG.update: + tmpSrc = 'OE' + assert tmpSrc in ['GG', 'OE'] + + if self[tmpSrc].isRecurrence: + if self[tmpSrc].status: + self.OP = Update(tmpSrc, 'Only need to update, because i\'m active') + else: + self.OP = Exclude(tmpSrc, 'Need to Exclude (Me = First event from recurrence) from recurrence') + + elif self[tmpSrc].isInstance: + self.OP = Update(tmpSrc, 'Only need to update, because already an exclu') + else: + self.OP = Update(tmpSrc, 'Simply Update... I\'m a single event') + else: + if not self.OE.synchro or self.OE.synchro.split('.')[0] < self.OE.update.split('.')[0]: + self.OP = Update('OE', 'Event already updated by another user, but not synchro with my google calendar') + else: + self.OP = NothingToDo("", 'Not update needed') + else: + self.OP = NothingToDo("", "Both are already deleted") + + # New in Odoo... Create on create_events of synchronize function + elif self.OE.found and not self.GG.found: + if self.OE.status: + self.OP = Delete('OE', 'Update or delete from GOOGLE') + else: + if not modeFull: + self.OP = Delete('GG', 'Deleted from Odoo, need to delete it from Gmail if already created') + else: + self.OP = NothingToDo("", "Already Deleted in gmail and unlinked in Odoo") + elif self.GG.found and not self.OE.found: + tmpSrc = 'GG' + if not self.GG.status and not self.GG.isInstance: + # don't need to make something... because event has been created and deleted before the synchronization + self.OP = NothingToDo("", 'Nothing to do... Create and Delete directly') + else: + if self.GG.isInstance: + if self[tmpSrc].status: + self.OP = Exclude(tmpSrc, 'Need to create the new exclu') + else: + self.OP = Exclude(tmpSrc, 'Need to copy and Exclude') + else: + self.OP = Create(tmpSrc, 'New EVENT CREATE from GMAIL') + + def __str__(self): + return self.__repr__() + + def __repr__(self): + event_str = "\n\n---- A SYNC EVENT ---" + event_str += "\n ID OE: %s " % (self.OE.event and self.OE.event.id) + event_str += "\n ID GG: %s " % (self.GG.event and self.GG.event.get('id', False)) + event_str += "\n Name OE: %s " % (self.OE.event and self.OE.event.name.encode('utf8')) + event_str += "\n Name GG: %s " % (self.GG.event and self.GG.event.get('summary', '').encode('utf8')) + event_str += "\n Found OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found) + event_str += "\n Recurrence OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence) + event_str += "\n Instance OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance) + event_str += "\n Synchro OE: %10s " % (self.OE.synchro) + event_str += "\n Update OE: %10s " % (self.OE.update) + event_str += "\n Update GG: %10s " % (self.GG.update) + event_str += "\n Status OE:%5s vs GG: %5s" % (self.OE.status, self.GG.status) + if (self.OP is None): + event_str += "\n Action %s" % "---!!!---NONE---!!!---" + else: + event_str += "\n Action %s" % type(self.OP).__name__ + event_str += "\n Source %s" % (self.OP.src) + event_str += "\n comment %s" % (self.OP.info) + return event_str + + +class SyncOperation(object): + def __init__(self, src, info, **kw): + self.src = src + self.info = info + for key, val in kw.items(): + setattr(self, key, val) + + def __str__(self): + return 'in__STR__' + + +class Create(SyncOperation): + pass + + +class Update(SyncOperation): + pass + + +class Delete(SyncOperation): + pass + + +class NothingToDo(SyncOperation): + pass + + +class Exclude(SyncOperation): + pass + + +class GoogleCalendar(models.AbstractModel): + STR_SERVICE = 'calendar' + _name = 'google.%s' % STR_SERVICE + + def generate_data(self, event, isCreating=False): + if event.allday: + start_date = fields.Datetime.context_timestamp(self, fields.Datetime.from_string(event.start)).isoformat('T').split('T')[0] + final_date = fields.Datetime.context_timestamp(self, fields.Datetime.from_string(event.stop) + timedelta(days=1)).isoformat('T').split('T')[0] + type = 'date' + vstype = 'dateTime' + else: + start_date = fields.Datetime.context_timestamp(self, fields.Datetime.from_string(event.start)).isoformat('T') + final_date = fields.Datetime.context_timestamp(self, fields.Datetime.from_string(event.stop)).isoformat('T') + type = 'dateTime' + vstype = 'date' + attendee_list = [] + for attendee in event.attendee_ids: + email = tools.email_split(attendee.email) + email = email[0] if email else 'NoEmail@mail.com' + attendee_list.append({ + 'email': email, + 'displayName': attendee.partner_id.name, + 'responseStatus': attendee.state or 'needsAction', + }) + + reminders = [] + for alarm in event.alarm_ids: + reminders.append({ + "method": "email" if alarm.type == "email" else "popup", + "minutes": alarm.duration_minutes + }) + data = { + "summary": event.name or '', + "description": event.description or '', + "start": { + type: start_date, + vstype: None, + 'timeZone': self.env.context.get('tz') or 'UTC', + }, + "end": { + type: final_date, + vstype: None, + 'timeZone': self.env.context.get('tz') or 'UTC', + }, + "attendees": attendee_list, + "reminders": { + "overrides": reminders, + "useDefault": "false" + }, + "location": event.location or '', + "visibility": event['privacy'] or 'public', + } + if event.recurrency and event.rrule: + data["recurrence"] = ["RRULE:" + event.rrule] + + if not event.active: + data["state"] = "cancelled" + + if not self.get_need_synchro_attendee(): + data.pop("attendees") + if isCreating: + other_google_ids = [other_att.google_internal_event_id for other_att in event.attendee_ids if other_att.google_internal_event_id] + if other_google_ids: + data["id"] = other_google_ids[0] + return data + + def create_an_event(self, event): + """ Create a new event in google calendar from the given event in Odoo. + :param event : record of calendar.event to export to google calendar + """ + data = self.generate_data(event, isCreating=True) + + url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urllib2.quote('id,updated'), self.get_token()) + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + data_json = json.dumps(data) + return self.env['google.service']._do_request(url, data_json, headers, type='POST') + + def delete_an_event(self, event_id): + """ Delete the given event in primary calendar of google cal. + :param event_id : google cal identifier of the event to delete + """ + params = { + 'access_token': self.get_token() + } + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event_id) + + return self.env['google.service']._do_request(url, params, headers, type='DELETE') + + def get_calendar_primary_id(self): + """ In google calendar, you can have multiple calendar. But only one is + the 'primary' one. This Calendar identifier is 'primary'. + """ + params = { + 'fields': 'id', + 'access_token': self.get_token() + } + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + + url = "/calendar/v3/calendars/primary" + + try: + status, content, ask_time = self.env['google.service']._do_request(url, params, headers, type='GET') + except urllib2.HTTPError, e: + if e.code == 401: # Token invalid / Acces unauthorized + error_msg = _("Your token is invalid or has been revoked !") + + self.env.user.write({'google_calendar_token': False, 'google_calendar_token_validity': False}) + self.env.cr.commit() + + raise self.env['res.config.settings'].get_config_warning(error_msg) + raise + + return (status_response(status), content['id'] or False, ask_time) + + def get_event_synchro_dict(self, lastSync=False, token=False, nextPageToken=False): + """ Returns events on the 'primary' calendar from google cal. + :returns dict where the key is the google_cal event id, and the value the details of the event, + defined at https://developers.google.com/google-apps/calendar/v3/reference/events/list + """ + if not token: + token = self.get_token() + + params = { + 'fields': 'items,nextPageToken', + 'access_token': token, + 'maxResults': 1000, + } + + if lastSync: + params['updatedMin'] = lastSync.strftime("%Y-%m-%dT%H:%M:%S.%fz") + params['showDeleted'] = True + else: + params['timeMin'] = self.get_minTime().strftime("%Y-%m-%dT%H:%M:%S.%fz") + + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + + url = "/calendar/v3/calendars/%s/events" % 'primary' + if nextPageToken: + params['pageToken'] = nextPageToken + + status, content, ask_time = self.env['google.service']._do_request(url, params, headers, type='GET') + + google_events_dict = {} + for google_event in content['items']: + google_events_dict[google_event['id']] = google_event + + if content.get('nextPageToken'): + google_events_dict.update( + self.get_event_synchro_dict(lastSync=lastSync, token=token, nextPageToken=content['nextPageToken']) + ) + + return google_events_dict + + def get_one_event_synchro(self, google_id): + token = self.get_token() + + params = { + 'access_token': token, + 'maxResults': 1000, + 'showDeleted': True, + } + + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + + url = "/calendar/v3/calendars/%s/events/%s" % ('primary', google_id) + try: + status, content, ask_time = self.env['google.service']._do_request(url, params, headers, type='GET') + except Exception, e: + _logger.info("Calendar Synchro - In except of get_one_event_synchro") + _logger.info(exception_to_unicode(e)) + return False + + return status_response(status) and content or False + + def update_to_google(self, oe_event, google_event): + url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'], 'id,updated', self.get_token()) + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + data = self.generate_data(oe_event) + data['sequence'] = google_event.get('sequence', 0) + data_json = json.dumps(data) + + status, content, ask_time = self.env['google.service']._do_request(url, data_json, headers, type='PATCH') + + update_date = datetime.strptime(content['updated'], "%Y-%m-%dT%H:%M:%S.%fz") + oe_event.write({'oe_update_date': update_date}) + + if self.env.context.get('curr_attendee'): + self.env['calendar.attendee'].browse(self.env.context['curr_attendee']).write({'oe_synchro_date': update_date}) + + def update_an_event(self, event): + data = self.generate_data(event) + url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event.google_internal_event_id) + headers = {} + data['access_token'] = self.get_token() + + status, response, ask_time = self.env['google.service']._do_request(url, data, headers, type='GET') + #TO_CHECK : , if http fail, no event, do DELETE ? + return response + + def update_recurrent_event_exclu(self, instance_id, event_ori_google_id, event_new): + """ Update event on google calendar + :param instance_id : new google cal identifier + :param event_ori_google_id : origin google cal identifier + :param event_new : record of calendar.event to modify + """ + data = self.generate_data(event_new) + url = "/calendar/v3/calendars/%s/events/%s?access_token=%s" % ('primary', instance_id, self.get_token()) + headers = {'Content-type': 'application/json'} + + data.update(recurringEventId=event_ori_google_id, originalStartTime=event_new.recurrent_id_date, sequence=self.get_sequence(instance_id)) + data_json = json.dumps(data) + return self.env['google.service']._do_request(url, data_json, headers, type='PUT') + + def update_from_google(self, event, single_event_dict, type): + """ Update an event in Odoo with information from google calendar + :param event : record od calendar.event to update + :param single_event_dict : dict of google cal event data + """ + CalendarEvent = self.env['calendar.event'].with_context(no_mail_to_attendees=True) + ResPartner = self.env['res.partner'] + CalendarAlarm = self.env['calendar.alarm'] + attendee_record = [] + alarm_record = set() + partner_record = [(4, self.env.user.partner_id.id)] + result = {} + + if self.get_need_synchro_attendee(): + for google_attendee in single_event_dict.get('attendees', []): + partner_email = google_attendee.get('email') + if type == "write": + for oe_attendee in event['attendee_ids']: + if oe_attendee.email == google_attendee['email']: + oe_attendee.write({'state': google_attendee['responseStatus'], 'google_internal_event_id': single_event_dict.get('id')}) + google_attendee['found'] = True + continue + + if google_attendee.get('found'): + continue + + attendee = ResPartner.search([('email', '=', google_attendee['email'])], limit=1) + if not attendee: + data = { + 'email': partner_email, + 'customer': False, + 'name': google_attendee.get("displayName", False) or partner_email + } + attendee = ResPartner.create(data) + attendee = attendee.read(['email'])[0] + partner_record.append((4, attendee.get('id'))) + attendee['partner_id'] = attendee.pop('id') + attendee['state'] = google_attendee['responseStatus'] + attendee_record.append((0, 0, attendee)) + for google_alarm in single_event_dict.get('reminders', {}).get('overrides', []): + alarm = CalendarAlarm.search( + [ + ('type', '=', google_alarm['method'] if google_alarm['method'] == 'email' else 'notification'), + ('duration_minutes', '=', google_alarm['minutes']) + ], limit=1 + ) + if not alarm: + data = { + 'type': google_alarm['method'] if google_alarm['method'] == 'email' else 'notification', + 'duration': google_alarm['minutes'], + 'interval': 'minutes', + 'name': "%s minutes - %s" % (google_alarm['minutes'], google_alarm['method']) + } + alarm = CalendarAlarm.create(data) + alarm_record.add(alarm.id) + + UTC = pytz.timezone('UTC') + if single_event_dict.get('start') and single_event_dict.get('end'): # If not cancelled + + if single_event_dict['start'].get('dateTime', False) and single_event_dict['end'].get('dateTime', False): + date = parser.parse(single_event_dict['start']['dateTime']) + stop = parser.parse(single_event_dict['end']['dateTime']) + date = str(date.astimezone(UTC))[:-6] + stop = str(stop.astimezone(UTC))[:-6] + allday = False + else: + date = single_event_dict['start']['date'] + stop = single_event_dict['end']['date'] + d_end = fields.Date.from_string(stop) + allday = True + d_end = d_end + timedelta(days=-1) + stop = fields.Date.to_string(d_end) + + update_date = datetime.strptime(single_event_dict['updated'], "%Y-%m-%dT%H:%M:%S.%fz") + result.update({ + 'start': date, + 'stop': stop, + 'allday': allday + }) + result.update({ + 'attendee_ids': attendee_record, + 'partner_ids': list(set(partner_record)), + 'alarm_ids': [(6, 0, list(alarm_record))], + + 'name': single_event_dict.get('summary', 'Event'), + 'description': single_event_dict.get('description', False), + 'location': single_event_dict.get('location', False), + 'privacy': single_event_dict.get('visibility', 'public'), + 'oe_update_date': update_date, + }) + + if single_event_dict.get("recurrence", False): + rrule = [rule for rule in single_event_dict["recurrence"] if rule.startswith("RRULE:")][0][6:] + result['rrule'] = rrule + if type == "write": + res = CalendarEvent.browse(event['id']).write(result) + elif type == "copy": + result['recurrence'] = True + res = CalendarEvent.browse([event['id']]).write(result) + elif type == "create": + res = CalendarEvent.create(result).id + + if self.env.context.get('curr_attendee'): + self.env['calendar.attendee'].with_context(no_mail_to_attendees=True).browse([self.env.context['curr_attendee']]).write({'oe_synchro_date': update_date, 'google_internal_event_id': single_event_dict.get('id', False)}) + return res + + def remove_references(self): + current_user = self.env.user + reset_data = { + 'google_calendar_rtoken': False, + 'google_calendar_token': False, + 'google_calendar_token_validity': False, + 'google_calendar_last_sync_date': False, + 'google_calendar_cal_id': False, + } + + all_my_attendees = self.env['calendar.attendee'].search([('partner_id', '=', current_user.partner_id.id)]) + all_my_attendees.write({'oe_synchro_date': False, 'google_internal_event_id': False}) + return current_user.write(reset_data) + + @api.model + def synchronize_events_cron(self): + """ Call by the cron. """ + users = self.env['res.users'].search([('google_calendar_last_sync_date', '!=', False)]) + _logger.info("Calendar Synchro - Started by cron") + + for user_to_sync in users.ids: + _logger.info("Calendar Synchro - Starting synchronization for a new user [%s]", user_to_sync) + try: + resp = self.sudo(user_to_sync).synchronize_events(lastSync=True) + if resp.get("status") == "need_reset": + _logger.info("[%s] Calendar Synchro - Failed - NEED RESET !", user_to_sync) + else: + _logger.info("[%s] Calendar Synchro - Done with status : %s !", user_to_sync, resp.get("status")) + except Exception, e: + _logger.info("[%s] Calendar Synchro - Exception : %s !", user_to_sync, exception_to_unicode(e)) + _logger.info("Calendar Synchro - Ended by cron") + + def synchronize_events(self, lastSync=True): + """ This method should be called as the user to sync. """ + user_to_sync = self.ids and self.ids[0] or self.env.uid + current_user = self.env['res.users'].sudo().browse(user_to_sync) + + recs = self.sudo(user_to_sync) + status, current_google, ask_time = recs.get_calendar_primary_id() + if current_user.google_calendar_cal_id: + if current_google != current_user.google_calendar_cal_id: + return { + "status": "need_reset", + "info": { + "old_name": current_user.google_calendar_cal_id, + "new_name": current_google + }, + "url": '' + } + + if lastSync and recs.get_last_sync_date() and not recs.get_disable_since_synchro(): + lastSync = recs.get_last_sync_date() + _logger.info("[%s] Calendar Synchro - MODE SINCE_MODIFIED : %s !", user_to_sync, fields.Datetime.to_string(lastSync)) + else: + lastSync = False + _logger.info("[%s] Calendar Synchro - MODE FULL SYNCHRO FORCED", user_to_sync) + else: + current_user.write({'google_calendar_cal_id': current_google}) + lastSync = False + _logger.info("[%s] Calendar Synchro - MODE FULL SYNCHRO - NEW CAL ID", user_to_sync) + + new_ids = [] + new_ids += recs.create_new_events() + new_ids += recs.bind_recurring_events_to_google() + + res = recs.update_events(lastSync) + + current_user.write({'google_calendar_last_sync_date': ask_time}) + return { + "status": res and "need_refresh" or "no_new_event_from_google", + "url": '' + } + + def create_new_events(self): + """ Create event in google calendar for the event not already + synchronized, for the current user. + :returns list of new created event identifier in google calendar + """ + new_ids = [] + my_partner_id = self.env.user.partner_id.id + + my_attendees = self.env['calendar.attendee'].with_context(virtual_id=False).search([('partner_id', '=', my_partner_id), + ('google_internal_event_id', '=', False), + '|', + ('event_id.stop', '>', fields.Datetime.to_string(self.get_minTime())), + ('event_id.final_date', '>', fields.Datetime.to_string(self.get_minTime())), + ]) + for att in my_attendees: + other_google_ids = [other_att.google_internal_event_id for other_att in att.event_id.attendee_ids if other_att.google_internal_event_id and other_att.id != att.id] + for other_google_id in other_google_ids: + if self.get_one_event_synchro(other_google_id): + att.write({'google_internal_event_id': other_google_id}) + break + else: + if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0: + status, response, ask_time = self.create_an_event(att.event_id) + if status_response(status): + update_date = datetime.strptime(response['updated'], "%Y-%m-%dT%H:%M:%S.%fz") + att.event_id.write({'oe_update_date': update_date}) + new_ids.append(response['id']) + att.write({'google_internal_event_id': response['id'], 'oe_synchro_date': update_date}) + self.env.cr.commit() + else: + _logger.warning("Impossible to create event %s. [%s] Enable DEBUG for response detail.", att.event_id.id, status) + _logger.debug("Response : %s", response) + return new_ids + + def get_context_no_virtual(self): + """ get the current context modified to prevent virtual ids and active test. """ + return dict(self.env.context, virtual_id=False, active_test=False) + + def bind_recurring_events_to_google(self): + new_ids = [] + CalendarAttendee = self.env['calendar.attendee'] + my_partner_id = self.env.user.partner_id.id + context_norecurrent = self.get_context_no_virtual() + my_attendees = CalendarAttendee.with_context(context_norecurrent).search([('partner_id', '=', my_partner_id), ('google_internal_event_id', '=', False)]) + for att in my_attendees: + new_google_internal_event_id = False + source_event_record = self.env['calendar.event'].browse(att.event_id.recurrent_id) + source_attendee_record = CalendarAttendee.search([('partner_id', '=', my_partner_id), ('event_id', '=', source_event_record.id)], limit=1) + if not source_attendee_record: + continue + + if att.event_id.recurrent_id_date and source_event_record.allday and source_attendee_record.google_internal_event_id: + new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.split(' ')[0].replace('-', '') + elif att.event_id.recurrent_id_date and source_attendee_record.google_internal_event_id: + new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.replace('-', '').replace(' ', 'T').replace(':', '') + 'Z' + + if new_google_internal_event_id: + #TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE ! + try: + status, response, ask_time = self.update_recurrent_event_exclu(new_google_internal_event_id, source_attendee_record.google_internal_event_id, att.event_id) + if status_response(status): + att.write({'google_internal_event_id': new_google_internal_event_id}) + new_ids.append(new_google_internal_event_id) + self.env.cr.commit() + else: + _logger.warning("Impossible to create event %s. [%s]", att.event_id.id, status) + _logger.debug("Response : %s", response) + except: + pass + return new_ids + + def update_events(self, lastSync=False): + """ Synchronze events with google calendar : fetching, creating, updating, deleting, ... """ + CalendarEvent = self.env['calendar.event'] + CalendarAttendee = self.env['calendar.attendee'] + my_partner_id = self.env.user.partner_id.id + context_novirtual = self.get_context_no_virtual() + + if lastSync: + try: + all_event_from_google = self.get_event_synchro_dict(lastSync=lastSync) + except urllib2.HTTPError, e: + if e.code == 410: # GONE, Google is lost. + # we need to force the rollback from this cursor, because it locks my res_users but I need to write in this tuple before to raise. + self.env.cr.rollback() + self.env.user.write({'google_calendar_last_sync_date': False}) + self.env.cr.commit() + error_key = json.loads(str(e)) + error_key = error_key.get('error', {}).get('message', 'nc') + error_msg = _("Google is lost... the next synchro will be a full synchro. \n\n %s") % error_key + raise self.env['res.config.settings'].get_config_warning(error_msg) + + my_google_attendees = CalendarAttendee.with_context(context_novirtual).search([ + ('partner_id', '=', my_partner_id), + ('google_internal_event_id', 'in', all_event_from_google.keys()) + ]) + my_google_att_ids = my_google_attendees.ids + + my_odoo_attendees = CalendarAttendee.with_context(context_novirtual).search([ + ('partner_id', '=', my_partner_id), + ('event_id.oe_update_date', '>', lastSync and fields.Datetime.to_string(lastSync) or self.get_minTime().fields.Datetime.to_string()), + ('google_internal_event_id', '!=', False), + ]) + + my_odoo_googleinternal_records = my_odoo_attendees.read(['google_internal_event_id', 'event_id']) + + if self.get_print_log(): + _logger.info("Calendar Synchro - \n\nUPDATE IN GOOGLE\n%s\n\nRETRIEVE FROM OE\n%s\n\nUPDATE IN OE\n%s\n\nRETRIEVE FROM GG\n%s\n\n", all_event_from_google, my_google_att_ids, my_odoo_attendees.ids, my_odoo_googleinternal_records) + + for gi_record in my_odoo_googleinternal_records: + active = True # if not sure, we request google + if gi_record.get('event_id'): + active = CalendarEvent.with_context(context_novirtual).browse(int(gi_record.get('event_id')[0])).active + + if gi_record.get('google_internal_event_id') and not all_event_from_google.get(gi_record.get('google_internal_event_id')) and active: + one_event = self.get_one_event_synchro(gi_record.get('google_internal_event_id')) + if one_event: + all_event_from_google[one_event['id']] = one_event + + my_attendees = (my_google_attendees | my_odoo_attendees) + + else: + domain = [ + ('partner_id', '=', my_partner_id), + ('google_internal_event_id', '!=', False), + '|', + ('event_id.stop', '>', fields.Datetime.to_string(self.get_minTime())), + ('event_id.final_date', '>', fields.Datetime.to_string(self.get_minTime())), + ] + + # Select all events from Odoo which have been already synchronized in gmail + my_attendees = CalendarAttendee.with_context(context_novirtual).search(domain) + all_event_from_google = self.get_event_synchro_dict(lastSync=False) + + event_to_synchronize = {} + for att in my_attendees: + event = att.event_id + + base_event_id = att.google_internal_event_id.rsplit('_', 1)[0] + + if base_event_id not in event_to_synchronize: + event_to_synchronize[base_event_id] = {} + + if att.google_internal_event_id not in event_to_synchronize[base_event_id]: + event_to_synchronize[base_event_id][att.google_internal_event_id] = SyncEvent() + + ev_to_sync = event_to_synchronize[base_event_id][att.google_internal_event_id] + + ev_to_sync.OE.attendee_id = att.id + ev_to_sync.OE.event = event + ev_to_sync.OE.found = True + ev_to_sync.OE.event_id = event.id + ev_to_sync.OE.isRecurrence = event.recurrency + ev_to_sync.OE.isInstance = bool(event.recurrent_id and event.recurrent_id > 0) + ev_to_sync.OE.update = event.oe_update_date + ev_to_sync.OE.status = event.active + ev_to_sync.OE.synchro = att.oe_synchro_date + + for event in all_event_from_google.values(): + event_id = event.get('id') + base_event_id = event_id.rsplit('_', 1)[0] + + if base_event_id not in event_to_synchronize: + event_to_synchronize[base_event_id] = {} + + if event_id not in event_to_synchronize[base_event_id]: + event_to_synchronize[base_event_id][event_id] = SyncEvent() + + ev_to_sync = event_to_synchronize[base_event_id][event_id] + + ev_to_sync.GG.event = event + ev_to_sync.GG.found = True + ev_to_sync.GG.isRecurrence = bool(event.get('recurrence', '')) + ev_to_sync.GG.isInstance = bool(event.get('recurringEventId', 0)) + ev_to_sync.GG.update = event.get('updated', None) # if deleted, no date without browse event + if ev_to_sync.GG.update: + ev_to_sync.GG.update = ev_to_sync.GG.update.replace('T', ' ').replace('Z', '') + ev_to_sync.GG.status = (event.get('status') != 'cancelled') + + ###################### + # PRE-PROCESSING # + ###################### + for base_event in event_to_synchronize: + for current_event in event_to_synchronize[base_event]: + event_to_synchronize[base_event][current_event].compute_OP(modeFull=not lastSync) + if self.get_print_log(): + if not isinstance(event_to_synchronize[base_event][current_event].OP, NothingToDo): + _logger.info(event_to_synchronize[base_event]) + + ###################### + # DO ACTION # + ###################### + for base_event in event_to_synchronize: + event_to_synchronize[base_event] = sorted(event_to_synchronize[base_event].iteritems(), key=operator.itemgetter(0)) + for current_event in event_to_synchronize[base_event]: + self.env.cr.commit() + event = current_event[1] # event is an Sync Event ! + actToDo = event.OP + actSrc = event.OP.src + + recs = self.with_context(curr_attendee=event.OE.attendee_id) + + if isinstance(actToDo, NothingToDo): + continue + elif isinstance(actToDo, Create): + context_tmp = {'newMeeting': True} + if actSrc == 'GG': + res = recs.with_context(context_tmp).update_from_google(False, event.GG.event, "create") + event.OE.event_id = res + meeting = CalendarEvent.browse(res) + attendee_records = CalendarAttendee.search([('partner_id', '=', my_partner_id), ('event_id', '=', res)]) + attendee_records.with_context(context_tmp).write({'oe_synchro_date': meeting.oe_update_date, 'google_internal_event_id': event.GG.event['id']}) + elif actSrc == 'OE': + raise "Should be never here, creation for OE is done before update !" + #TODO Add to batch + elif isinstance(actToDo, Update): + if actSrc == 'GG': + recs.update_from_google(event.OE.event, event.GG.event, 'write') + elif actSrc == 'OE': + recs.update_to_google(event.OE.event, event.GG.event) + elif isinstance(actToDo, Exclude): + if actSrc == 'OE': + recs.delete_an_event(current_event[0]) + elif actSrc == 'GG': + new_google_event_id = event.GG.event['id'].rsplit('_', 1)[1] + if 'T' in new_google_event_id: + new_google_event_id = new_google_event_id.replace('T', '')[:-1] + else: + new_google_event_id = new_google_event_id + "000000" + + if event.GG.status: + parent_event = {} + if not event_to_synchronize[base_event][0][1].OE.event_id: + main_ev = CalendarAttendee.with_context(context_novirtual).search([('google_internal_event_id', '=', event.GG.event['id'].rsplit('_', 1)[0])], limit=1) + event_to_synchronize[base_event][0][1].OE.event_id = main_ev.event_id.id + + parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id) + res = recs.update_from_google(parent_event, event.GG.event, "copy") + else: + parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id + if parent_oe_id: + CalendarEvent.browse("%s-%s" % (parent_oe_id, new_google_event_id)).with_context(curr_attendee=event.OE.attendee_id).unlink(can_be_deleted=True) + + elif isinstance(actToDo, Delete): + if actSrc == 'GG': + try: + # if already deleted from gmail or never created + recs.delete_an_event(current_event[0]) + except Exception, e: + if e.code in (401, 410,): + pass + else: + raise e + elif actSrc == 'OE': + CalendarEvent.browse(event.OE.event_id).unlink(can_be_deleted=False) + return True + + def check_and_sync(self, oe_event, google_event): + if datetime.strptime(oe_event.oe_update_date, "%Y-%m-%d %H:%M:%S.%f") > datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"): + self.update_to_google(oe_event, google_event) + elif datetime.strptime(oe_event.oe_update_date, "%Y-%m-%d %H:%M:%S.%f") < datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"): + self.update_from_google(oe_event, google_event, 'write') + + def get_sequence(self, instance_id): + params = { + 'fields': 'sequence', + 'access_token': self.get_token() + } + headers = {'Content-type': 'application/json'} + url = "/calendar/v3/calendars/%s/events/%s" % ('primary', instance_id) + status, content, ask_time = self.env['google.service']._do_request(url, params, headers, type='GET') + return content.get('sequence', 0) + + ################################# + ## MANAGE CONNEXION TO GMAIL ## + ################################# + + def get_token(self): + current_user = self.env.user + if not current_user.google_calendar_token_validity or \ + fields.Datetime.from_string(current_user.google_calendar_token_validity.split('.')[0]) < (datetime.now() + timedelta(minutes=1)): + self.do_refresh_token() + current_user.refresh() + return current_user.google_calendar_token + + def get_last_sync_date(self): + current_user = self.env.user + return current_user.google_calendar_last_sync_date and fields.Datetime.from_string(current_user.google_calendar_last_sync_date) + timedelta(minutes=0) or False + + def do_refresh_token(self): + current_user = self.env.user + all_token = self.env['google.service']._refresh_google_token_json(current_user.google_calendar_rtoken, self.STR_SERVICE) + + vals = {} + vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in')) + vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') + + self.env.user.sudo().write(vals) + + def need_authorize(self): + current_user = self.env.user + return current_user.google_calendar_rtoken is False + + def get_calendar_scope(self, RO=False): + readonly = '.readonly' if RO else '' + return 'https://www.googleapis.com/auth/calendar%s' % (readonly) + + def authorize_google_uri(self, from_url='http://www.odoo.com'): + url = self.env['google.service']._get_authorize_uri(from_url, self.STR_SERVICE, scope=self.get_calendar_scope()) + return url + + def can_authorize_google(self): + return self.env['res.users'].has_group('base.group_erp_manager') + + @api.model + def set_all_tokens(self, authorization_code): + all_token = self.env['google.service']._get_google_token_json(authorization_code, self.STR_SERVICE) + + vals = {} + vals['google_%s_rtoken' % self.STR_SERVICE] = all_token.get('refresh_token') + vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in')) + vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') + self.env.user.sudo().write(vals) + + def get_minTime(self): + number_of_week = self.env['ir.config_parameter'].get_param('calendar.week_synchro', default=13) + return datetime.now() - timedelta(weeks=number_of_week) + + def get_need_synchro_attendee(self): + return self.env['ir.config_parameter'].get_param('calendar.block_synchro_attendee', default=True) + + def get_disable_since_synchro(self): + return self.env['ir.config_parameter'].get_param('calendar.block_since_synchro', default=False) + + def get_print_log(self): + return self.env['ir.config_parameter'].get_param('calendar.debug_print', default=False) diff --git a/addons/google_calendar/models/res_config.py b/addons/google_calendar/models/res_config.py new file mode 100644 index 00000000000..f1d93ad4046 --- /dev/null +++ b/addons/google_calendar/models/res_config.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class CalendarSettings(models.TransientModel): + + _inherit = 'base.config.settings' + + google_cal_sync = fields.Boolean("Show Tutorial") + cal_client_id = fields.Char("Client_id") + cal_client_secret = fields.Char("Client_key") + server_uri = fields.Char('URI for tuto') + + def set_calset(self): + self.env['ir.config_parameter'].set_param('google_calendar_client_id', (self.cal_client_id or '').strip(), groups=['base.group_system']) + self.env['ir.config_parameter'].set_param('google_calendar_client_secret', (self.cal_client_secret or '').strip(), groups=['base.group_system']) + + def get_default_all(self, fields): + cal_client_id = self.env['ir.config_parameter'].get_param('google_calendar_client_id', default='') + cal_client_secret = self.env['ir.config_parameter'].get_param('google_calendar_client_secret', default='') + server_uri = "%s/google_account/authentication" % self.env['ir.config_parameter'].get_param('web.base.url', default="http://yourcompany.odoo.com") + return dict(cal_client_id=cal_client_id, cal_client_secret=cal_client_secret, server_uri=server_uri) diff --git a/addons/google_calendar/models/res_users.py b/addons/google_calendar/models/res_users.py new file mode 100644 index 00000000000..290606dfe80 --- /dev/null +++ b/addons/google_calendar/models/res_users.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class User(models.Model): + + _inherit = 'res.users' + + google_calendar_rtoken = fields.Char('Refresh Token', copy=False) + google_calendar_token = fields.Char('User token', copy=False) + google_calendar_token_validity = fields.Datetime('Token Validity', copy=False) + google_calendar_last_sync_date = fields.Datetime('Last synchro date', copy=False) + google_calendar_cal_id = fields.Char('Calendar ID', copy=False, help='Last Calendar ID who has been synchronized. If it is changed, we remove all links between GoogleID and Odoo Google Internal ID') diff --git a/addons/google_calendar/res_config.py b/addons/google_calendar/res_config.py deleted file mode 100644 index 412f66122cd..00000000000 --- a/addons/google_calendar/res_config.py +++ /dev/null @@ -1,29 +0,0 @@ -from openerp.osv import fields, osv - -class calendar_config_settings(osv.TransientModel): - _inherit = 'base.config.settings' - - _columns = { - 'google_cal_sync': fields.boolean("Show Tutorial"), - 'cal_client_id': fields.char("Client_id"), - 'cal_client_secret': fields.char("Client_key"), - 'server_uri': fields.char('URI for tuto') - } - - def set_calset(self, cr, uid, ids, context=None) : - params = self.pool['ir.config_parameter'] - myself = self.browse(cr, uid, ids[0], context=context) - params.set_param(cr, uid, 'google_calendar_client_id', (myself.cal_client_id or '').strip(), groups=['base.group_system'], context=None) - params.set_param(cr, uid, 'google_calendar_client_secret', (myself.cal_client_secret or '').strip(), groups=['base.group_system'], context=None) - - - def get_default_all(self, cr, uid, fields, context=None): - params = self.pool.get('ir.config_parameter') - cal_client_id = params.get_param(cr, uid, 'google_calendar_client_id',default='',context=context) - cal_client_secret = params.get_param(cr, uid, 'google_calendar_client_secret',default='',context=context) - server_uri= "%s/google_account/authentication" % params.get_param(cr, uid, 'web.base.url',default="http://yourcompany.odoo.com",context=context) - return dict(cal_client_id=cal_client_id,cal_client_secret=cal_client_secret,server_uri=server_uri) - - - - diff --git a/addons/google_calendar/static/src/js/calendar_sync.js b/addons/google_calendar/static/src/js/google_calendar.js similarity index 100% rename from addons/google_calendar/static/src/js/calendar_sync.js rename to addons/google_calendar/static/src/js/google_calendar.js diff --git a/addons/google_calendar/views/google_calendar.xml b/addons/google_calendar/views/google_calendar_templates.xml similarity index 89% rename from addons/google_calendar/views/google_calendar.xml rename to addons/google_calendar/views/google_calendar_templates.xml index f258b8f0c5c..6f8924e4af8 100644 --- a/addons/google_calendar/views/google_calendar.xml +++ b/addons/google_calendar/views/google_calendar_templates.xml @@ -3,7 +3,7 @@ diff --git a/addons/google_calendar/res_config_view.xml b/addons/google_calendar/views/res_config_views.xml similarity index 90% rename from addons/google_calendar/res_config_view.xml rename to addons/google_calendar/views/res_config_views.xml index 97ccfe5c0df..1266d66871b 100644 --- a/addons/google_calendar/res_config_view.xml +++ b/addons/google_calendar/views/res_config_views.xml @@ -1,7 +1,6 @@ - - - + + Calendar_settings_grgrgrgt base.config.settings @@ -12,9 +11,9 @@

To setup the signin process with Google, first you have to perform the following steps

-
    +
    • Connect on your google account and go to https://console.developers.google.com/
    • -
    • +
    • Click on 'Create a project...' and enter a project name and change your id if you want. Don't forget to accept the Terms of Services

    • @@ -24,8 +23,8 @@



      - -
    • +
    • +
    • In the menu on left side, select the sub menu 'Credentials' (from menu APIs and auth) and click on button 'Create new Client ID'

    • @@ -42,13 +41,13 @@
    • Once done, you will have the both informations (Client ID and Client Secret) that you need to insert in the 2 fields below!

    • - Return at Top + Return at Top