[MERGE] forward port of branch saas-4 up to 5087612
[odoo/odoo.git] / addons / google_calendar / google_calendar.py
1 # -*- coding: utf-8 -*-
2
3 import operator
4 import simplejson
5 import urllib2
6
7 import openerp
8 from openerp import tools
9 from openerp import SUPERUSER_ID
10 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
11 from openerp.tools.translate import _
12 from openerp.http import request
13 from datetime import datetime, timedelta
14 from dateutil import parser
15 import pytz
16 from openerp.osv import fields, osv
17
18 import logging
19 _logger = logging.getLogger(__name__)
20
21
22 def status_response(status, substr=False):
23     if substr:
24         return int(str(status)[0])
25     else:
26         return status_response(status, substr=True) == 2
27
28
29 class Meta(type):
30     """ This Meta class allow to define class as a structure, and so instancied variable
31         in __init__ to avoid to have side effect alike 'static' variable """
32     def __new__(typ, name, parents, attrs):
33         methods = dict((k, v) for k, v in attrs.iteritems()
34                        if callable(v))
35         attrs = dict((k, v) for k, v in attrs.iteritems()
36                      if not callable(v))
37
38         def init(self, **kw):
39             for k, v in attrs.iteritems():
40                 setattr(self, k, v)
41             for k, v in kw.iteritems():
42                 assert k in attrs
43                 setattr(self, k, v)
44
45         methods['__init__'] = init
46         methods['__getitem__'] = getattr
47         return type.__new__(typ, name, parents, methods)
48
49
50 class Struct(object):
51     __metaclass__ = Meta
52
53
54 class OpenerpEvent(Struct):
55         event = False
56         found = False
57         event_id = False
58         isRecurrence = False
59         isInstance = False
60         update = False
61         status = False
62         attendee_id = False
63         synchro = False
64
65
66 class GmailEvent(Struct):
67     event = False
68     found = False
69     isRecurrence = False
70     isInstance = False
71     update = False
72     status = False
73
74
75 class SyncEvent(object):
76     def __init__(self):
77         self.OE = OpenerpEvent()
78         self.GG = GmailEvent()
79         self.OP = None
80
81     def __getitem__(self, key):
82         return getattr(self, key)
83
84     def compute_OP(self, modeFull=True):
85         #If event are already in Gmail and in OpenERP
86         if self.OE.found and self.GG.found:
87             #If the event has been deleted from one side, we delete on other side !
88             if self.OE.status != self.GG.status:
89                 self.OP = Delete((self.OE.status and "OE") or (self.GG.status and "GG"),
90                                  'The event has been deleted from one side, we delete on other side !')
91             #If event is not deleted !
92             elif self.OE.status and self.GG.status:
93                 if self.OE.update.split('.')[0] != self.GG.update.split('.')[0]:
94                     if self.OE.update < self.GG.update:
95                         tmpSrc = 'GG'
96                     elif self.OE.update > self.GG.update:
97                         tmpSrc = 'OE'
98                     assert tmpSrc in ['GG', 'OE']
99
100                     #if self.OP.action == None:
101                     if self[tmpSrc].isRecurrence:
102                         if self[tmpSrc].status:
103                             self.OP = Update(tmpSrc, 'Only need to update, because i\'m active')
104                         else:
105                             self.OP = Exclude(tmpSrc, 'Need to Exclude (Me = First event from recurrence) from recurrence')
106
107                     elif self[tmpSrc].isInstance:
108                         self.OP = Update(tmpSrc, 'Only need to update, because already an exclu')
109                     else:
110                         self.OP = Update(tmpSrc, 'Simply Update... I\'m a single event')
111                 else:
112                     if not self.OE.synchro or self.OE.synchro.split('.')[0] < self.OE.update.split('.')[0]:
113                         self.OP = Update('OE', 'Event already updated by another user, but not synchro with my google calendar')
114                     else:
115                         self.OP = NothingToDo("", 'Not update needed')
116             else:
117                 self.OP = NothingToDo("", "Both are already deleted")
118
119         # New in openERP...  Create on create_events of synchronize function
120         elif self.OE.found and not self.GG.found:
121             if self.OE.status:
122                 self.OP = Delete('OE', 'Update or delete from GOOGLE')
123             else:
124                 if not modeFull:
125                     self.OP = Delete('GG', 'Deleted from OpenERP, need to delete it from Gmail if already created')
126                 else:
127                     self.OP = NothingToDo("", "Already Deleted in gmail and unlinked in OpenERP")
128         elif self.GG.found and not self.OE.found:
129             tmpSrc = 'GG'
130             if not self.GG.status and not self.GG.isInstance:
131                 # don't need to make something... because event has been created and deleted before the synchronization
132                 self.OP = NothingToDo("", 'Nothing to do... Create and Delete directly')
133             else:
134                 if self.GG.isInstance:
135                     if self[tmpSrc].status:
136                         self.OP = Exclude(tmpSrc, 'Need to create the new exclu')
137                     else:
138                         self.OP = Exclude(tmpSrc, 'Need to copy and Exclude')
139                 else:
140                     self.OP = Create(tmpSrc, 'New EVENT CREATE from GMAIL')
141
142     def __str__(self):
143         return self.__repr__()
144
145     def __repr__(self):
146         myPrint = "\n\n---- A SYNC EVENT ---"
147         myPrint += "\n    ID          OE: %s " % (self.OE.event and self.OE.event.id)
148         myPrint += "\n    ID          GG: %s " % (self.GG.event and self.GG.event.get('id', False))
149         myPrint += "\n    Name        OE: %s " % (self.OE.event and self.OE.event.name.encode('utf8'))
150         myPrint += "\n    Name        GG: %s " % (self.GG.event and self.GG.event.get('summary', '').encode('utf8'))
151         myPrint += "\n    Found       OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found)
152         myPrint += "\n    Recurrence  OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence)
153         myPrint += "\n    Instance    OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance)
154         myPrint += "\n    Synchro     OE: %10s " % (self.OE.synchro)
155         myPrint += "\n    Update      OE: %10s " % (self.OE.update)
156         myPrint += "\n    Update      GG: %10s " % (self.GG.update)
157         myPrint += "\n    Status      OE:%5s vs GG: %5s" % (self.OE.status, self.GG.status)
158         if (self.OP is None):
159             myPrint += "\n    Action      %s" % "---!!!---NONE---!!!---"
160         else:
161             myPrint += "\n    Action      %s" % type(self.OP).__name__
162             myPrint += "\n    Source      %s" % (self.OP.src)
163             myPrint += "\n    comment     %s" % (self.OP.info)
164         return myPrint
165
166
167 class SyncOperation(object):
168     def __init__(self, src, info, **kw):
169         self.src = src
170         self.info = info
171         for k, v in kw.items():
172             setattr(self, k, v)
173
174     def __str__(self):
175         return 'in__STR__'
176
177
178 class Create(SyncOperation):
179     pass
180
181
182 class Update(SyncOperation):
183     pass
184
185
186 class Delete(SyncOperation):
187     pass
188
189
190 class NothingToDo(SyncOperation):
191     pass
192
193
194 class Exclude(SyncOperation):
195     pass
196
197
198 class google_calendar(osv.AbstractModel):
199     STR_SERVICE = 'calendar'
200     _name = 'google.%s' % STR_SERVICE
201
202     def generate_data(self, cr, uid, event, isCreating=False, context=None):
203         if event.allday:
204             start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.start, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T').split('T')[0]
205             final_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.start, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=event.duration) + timedelta(days=isCreating and 1 or 0), context=context).isoformat('T').split('T')[0]
206             type = 'date'
207             vstype = 'dateTime'
208         else:
209             start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.start, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T')
210             final_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.stop, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T')
211             type = 'dateTime'
212             vstype = 'date'
213         attendee_list = []
214
215         for attendee in event.attendee_ids:
216             attendee_list.append({
217                 'email': attendee.email or 'NoEmail@mail.com',
218                 'displayName': attendee.partner_id.name,
219                 'responseStatus': attendee.state or 'needsAction',
220             })
221         data = {
222             "summary": event.name or '',
223             "description": event.description or '',
224             "start": {
225                 type: start_date,
226                 vstype: None,
227                 'timeZone': 'UTC'
228             },
229             "end": {
230                 type: final_date,
231                 vstype: None,
232                 'timeZone': 'UTC'
233             },
234             "attendees": attendee_list,
235             "location": event.location or '',
236             "visibility": event['class'] or 'public',
237         }
238         if event.recurrency and event.rrule:
239             data["recurrence"] = ["RRULE:" + event.rrule]
240
241         if not event.active:
242             data["state"] = "cancelled"
243
244         if not self.get_need_synchro_attendee(cr, uid, context=context):
245             data.pop("attendees")
246
247         return data
248
249     def create_an_event(self, cr, uid, event, context=None):
250         gs_pool = self.pool['google.service']
251         data = self.generate_data(cr, uid, event, isCreating=True, context=context)
252
253         url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urllib2.quote('id,updated'), self.get_token(cr, uid, context))
254         headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
255         data_json = simplejson.dumps(data)
256
257         return gs_pool._do_request(cr, uid, url, data_json, headers, type='POST', context=context)
258
259     def delete_an_event(self, cr, uid, event_id, context=None):
260         gs_pool = self.pool['google.service']
261
262         params = {
263             'access_token': self.get_token(cr, uid, context)
264         }
265         headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
266         url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event_id)
267
268         return gs_pool._do_request(cr, uid, url, params, headers, type='DELETE', context=context)
269
270     def get_calendar_primary_id(self, cr, uid, context=None):
271         params = {
272             'fields': 'id',
273             'access_token': self.get_token(cr, uid, context)
274         }
275         headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
276
277         url = "/calendar/v3/calendars/primary"
278
279         try:
280             st, content = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context)
281         except Exception, e:
282
283             if (e.code == 401):  # Token invalid / Acces unauthorized
284                 error_msg = "Your token is invalid or has been revoked !"
285
286                 registry = openerp.modules.registry.RegistryManager.get(request.session.db)
287                 with registry.cursor() as cur:
288                     self.pool['res.users'].write(cur, uid, [uid], {'google_calendar_token': False, 'google_calendar_token_validity': False}, context=context)
289
290                 raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context)
291             raise
292
293         return status_response(st) and content['id'] or False
294
295     def get_event_synchro_dict(self, cr, uid, lastSync=False, token=False, nextPageToken=False, context=None):
296         if not token:
297             token = self.get_token(cr, uid, context)
298
299         params = {
300             'fields': 'items,nextPageToken',
301             'access_token': token,
302             'maxResults': 1000,
303             #'timeMin': self.get_minTime(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz"),
304         }
305
306         if lastSync:
307             params['updatedMin'] = lastSync.strftime("%Y-%m-%dT%H:%M:%S.%fz")
308             params['showDeleted'] = True
309         else:
310             params['timeMin'] = self.get_minTime(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz")
311
312         headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
313
314         url = "/calendar/v3/calendars/%s/events" % 'primary'
315         if nextPageToken:
316             params['pageToken'] = nextPageToken
317
318         status, content = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context)
319
320         google_events_dict = {}
321         for google_event in content['items']:
322             google_events_dict[google_event['id']] = google_event
323
324         if content.get('nextPageToken'):
325             google_events_dict.update(
326                 self.get_event_synchro_dict(cr, uid, lastSync=lastSync, token=token, nextPageToken=content['nextPageToken'], context=context)
327             )
328
329         return google_events_dict
330
331     def get_one_event_synchro(self, cr, uid, google_id, context=None):
332         token = self.get_token(cr, uid, context)
333
334         params = {
335             'access_token': token,
336             'maxResults': 1000,
337             'showDeleted': True,
338         }
339
340         headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
341
342         url = "/calendar/v3/calendars/%s/events/%s" % ('primary', google_id)
343         try:
344             status, content = self.pool['google.service']._do_request(cr, uid, url, params, headers, type='GET', context=context)
345         except:
346             _logger.info("Calendar Synchro - In except of get_one_event_synchro")
347             pass
348
349         return status_response(status) and content or False
350
351     def update_to_google(self, cr, uid, oe_event, google_event, context):
352         calendar_event = self.pool['calendar.event']
353
354         url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'], 'id,updated', self.get_token(cr, uid, context))
355         headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
356         data = self.generate_data(cr, uid, oe_event, context)
357         data['sequence'] = google_event.get('sequence', 0)
358         data_json = simplejson.dumps(data)
359
360         status, content = self.pool['google.service']._do_request(cr, uid, url, data_json, headers, type='PATCH', context=context)
361
362         update_date = datetime.strptime(content['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
363         calendar_event.write(cr, uid, [oe_event.id], {'oe_update_date': update_date})
364
365         if context['curr_attendee']:
366             self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date}, context)
367
368     def update_an_event(self, cr, uid, event, context=None):
369         data = self.generate_data(cr, uid, event, context=context)
370
371         url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event.google_internal_event_id)
372         headers = {}
373         data['access_token'] = self.get_token(cr, uid, context)
374
375         status, response = self.pool['google.service']._do_request(cr, uid, url, data, headers, type='GET', context=context)
376         #TO_CHECK : , if http fail, no event, do DELETE ?
377         return response
378
379     def update_recurrent_event_exclu(self, cr, uid, instance_id, event_ori_google_id, event_new, context=None):
380         gs_pool = self.pool['google.service']
381
382         data = self.generate_data(cr, uid, event_new, context=context)
383
384         data['recurringEventId'] = event_ori_google_id
385         data['originalStartTime'] = event_new.recurrent_id_date
386
387         url = "/calendar/v3/calendars/%s/events/%s?access_token=%s" % ('primary', instance_id, self.get_token(cr, uid, context))
388         headers = {'Content-type': 'application/json'}
389
390         data['sequence'] = self.get_sequence(cr, uid, instance_id, context)
391
392         data_json = simplejson.dumps(data)
393         return gs_pool._do_request(cr, uid, url, data_json, headers, type='PUT', context=context)
394
395     def update_from_google(self, cr, uid, event, single_event_dict, type, context):
396         if context is None:
397             context = []
398
399         calendar_event = self.pool['calendar.event']
400         res_partner_obj = self.pool['res.partner']
401         calendar_attendee_obj = self.pool['calendar.attendee']
402         user_obj = self.pool['res.users']
403         myPartnerID = user_obj.browse(cr, uid, uid, context).partner_id.id
404         attendee_record = []
405         partner_record = [(4, myPartnerID)]
406         result = {}
407
408         if single_event_dict.get('attendees', False):
409             for google_attendee in single_event_dict['attendees']:
410                 if type == "write":
411                     for oe_attendee in event['attendee_ids']:
412                         if oe_attendee.email == google_attendee['email']:
413                             calendar_attendee_obj.write(cr, uid, [oe_attendee.id], {'state': google_attendee['responseStatus']}, context=context)
414                             google_attendee['found'] = True
415                             continue
416
417                 if google_attendee.get('found', False):
418                     continue
419                 if self.get_need_synchro_attendee(cr, uid, context=context):
420                     attendee_id = res_partner_obj.search(cr, uid, [('email', '=', google_attendee['email'])], context=context)
421                     if not attendee_id:
422                         data = {
423                             'email': google_attendee['email'],
424                             'customer': False,
425                             'name': google_attendee.get("displayName", False) or google_attendee['email']
426                         }
427                         attendee_id = [res_partner_obj.create(cr, uid, data, context=context)]
428                     attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context)
429                     partner_record.append((4, attendee.get('id')))
430                     attendee['partner_id'] = attendee.pop('id')
431                     attendee['state'] = google_attendee['responseStatus']
432                     attendee_record.append((0, 0, attendee))
433
434         UTC = pytz.timezone('UTC')
435         if single_event_dict.get('start') and single_event_dict.get('end'):  # If not cancelled
436
437             if single_event_dict['start'].get('dateTime', False) and single_event_dict['end'].get('dateTime', False):
438                 date = parser.parse(single_event_dict['start']['dateTime'])
439                 stop = parser.parse(single_event_dict['end']['dateTime'])
440                 date = str(date.astimezone(UTC))[:-6]
441                 stop = str(stop.astimezone(UTC))[:-6]
442                 allday = False
443             else:
444                 date = (single_event_dict['start']['date'])
445                 stop = (single_event_dict['end']['date'])
446                 d_end = datetime.strptime(stop, DEFAULT_SERVER_DATE_FORMAT)
447                 allday = True
448                 d_end = d_end + timedelta(days=-1)
449                 stop = d_end.strftime(DEFAULT_SERVER_DATE_FORMAT)
450
451             update_date = datetime.strptime(single_event_dict['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
452             result.update({
453                 'start': date,
454                 'stop': stop,
455                 'allday': allday
456             })
457         result.update({
458             'attendee_ids': attendee_record,
459             'partner_ids': list(set(partner_record)),
460
461             'name': single_event_dict.get('summary', 'Event'),
462             'description': single_event_dict.get('description', False),
463             'location': single_event_dict.get('location', False),
464             'class': single_event_dict.get('visibility', 'public'),
465             'oe_update_date': update_date,
466         })
467
468         if single_event_dict.get("recurrence", False):
469             rrule = [rule for rule in single_event_dict["recurrence"] if rule.startswith("RRULE:")][0][6:]
470             result['rrule'] = rrule
471
472         if type == "write":
473             res = calendar_event.write(cr, uid, event['id'], result, context=context)
474         elif type == "copy":
475             result['recurrence'] = True
476             res = calendar_event.write(cr, uid, [event['id']], result, context=context)
477         elif type == "create":
478             res = calendar_event.create(cr, uid, result, context=context)
479
480         if context['curr_attendee']:
481             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)
482         return res
483
484     def remove_references(self, cr, uid, context=None):
485         current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
486         reset_data = {
487             'google_calendar_rtoken': False,
488             'google_calendar_token': False,
489             'google_calendar_token_validity': False,
490             'google_calendar_last_sync_date': False,
491             'google_calendar_cal_id': False,
492         }
493
494         all_my_attendees = self.pool['calendar.attendee'].search(cr, uid, [('partner_id', '=', current_user.partner_id.id)], context=context)
495         self.pool['calendar.attendee'].write(cr, uid, all_my_attendees, {'oe_synchro_date': False, 'google_internal_event_id': False}, context=context)
496         current_user.write(reset_data, context=context)
497         return True
498
499     def synchronize_events(self, cr, uid, ids, lastSync=True, context=None):
500         if context is None:
501             context = {}
502
503         # def isValidSync(syncToken):
504         #     gs_pool = self.pool['google.service']
505         #     params = {
506         #         'maxResults': 1,
507         #         'fields': 'id',
508         #         'access_token': self.get_token(cr, uid, context),
509         #         'syncToken': syncToken,
510         #     }
511         #     url = "/calendar/v3/calendars/primary/events"
512         #     status, response = gs_pool._do_request(cr, uid, url, params, type='GET', context=context)
513         #     return int(status) != 410
514
515         current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
516
517         context_with_time = dict(context.copy(), ask_time=True)
518         current_google = self.get_calendar_primary_id(cr, uid, context=context_with_time)
519
520         if current_user.google_calendar_cal_id:
521             if current_google != current_user.google_calendar_cal_id:
522                 return {
523                     "status": "need_reset",
524                     "info": {
525                         "old_name": current_user.google_calendar_cal_id,
526                         "new_name": current_google
527                     },
528                     "url": ''
529                 }
530
531             if lastSync and self.get_last_sync_date(cr, uid, context=context) and not self.get_disable_since_synchro(cr, uid, context=context):
532                 lastSync = self.get_last_sync_date(cr, uid, context)
533                 _logger.info("Calendar Synchro - MODE SINCE_MODIFIED : %s !" % lastSync.strftime(DEFAULT_SERVER_DATETIME_FORMAT))
534             else:
535                 lastSync = False
536                 _logger.info("Calendar Synchro - MODE FULL SYNCHRO FORCED")
537         else:
538             current_user.write({'google_calendar_cal_id': current_google}, context=context)
539             lastSync = False
540             _logger.info("Calendar Synchro - MODE FULL SYNCHRO - NEW CAL ID")
541
542         new_ids = []
543         new_ids += self.create_new_events(cr, uid, context=context)
544         new_ids += self.bind_recurring_events_to_google(cr, uid, context)
545
546         res = self.update_events(cr, uid, lastSync, context)
547
548         current_user.write({'google_calendar_last_sync_date': context_with_time.get('ask_time')}, context=context)
549         return {
550             "status": res and "need_refresh" or "no_new_event_form_google",
551             "url": ''
552         }
553
554     def create_new_events(self, cr, uid, context=None):
555         if context is None:
556             context = {}
557
558         new_ids = []
559         ev_obj = self.pool['calendar.event']
560         att_obj = self.pool['calendar.attendee']
561         user_obj = self.pool['res.users']
562         myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
563
564         context_norecurrent = context.copy()
565         context_norecurrent['virtual_id'] = False
566         my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID),
567                                     ('google_internal_event_id', '=', False),
568                                     '|',
569                                     ('event_id.stop', '>', self.get_minTime(cr, uid, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
570                                     ('event_id.final_date', '>', self.get_minTime(cr, uid, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
571                                     ], context=context_norecurrent)
572         for att in att_obj.browse(cr, uid, my_att_ids, context=context):
573             if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0:
574                 st, response = self.create_an_event(cr, uid, att.event_id, context=context)
575                 if status_response(st):
576                     update_date = datetime.strptime(response['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
577                     ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date': update_date})
578                     new_ids.append(response['id'])
579                     att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date': update_date})
580                     cr.commit()
581                 else:
582                     _logger.warning("Impossible to create event %s. [%s]" % (att.event_id.id, st))
583                     _logger.warning("Response : %s" % response)
584         return new_ids
585
586     def get_context_no_virtual(self, context):
587         context_norecurrent = context.copy()
588         context_norecurrent['virtual_id'] = False
589         context_norecurrent['active_test'] = False
590         return context_norecurrent
591
592     def bind_recurring_events_to_google(self, cr, uid, context=None):
593         if context is None:
594             context = {}
595
596         new_ids = []
597         ev_obj = self.pool['calendar.event']
598         att_obj = self.pool['calendar.attendee']
599         user_obj = self.pool['res.users']
600         myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
601
602         context_norecurrent = self.get_context_no_virtual(context)
603         my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('google_internal_event_id', '=', False)], context=context_norecurrent)
604
605         for att in att_obj.browse(cr, uid, my_att_ids, context=context):
606             if att.event_id.recurrent_id and att.event_id.recurrent_id > 0:
607                 new_google_internal_event_id = False
608                 source_event_record = ev_obj.browse(cr, uid, att.event_id.recurrent_id, context)
609                 source_attendee_record_id = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('event_id', '=', source_event_record.id)], context=context)
610                 source_attendee_record = att_obj.browse(cr, uid, source_attendee_record_id, context)[0]
611
612                 if att.event_id.recurrent_id_date and source_event_record.allday and source_attendee_record.google_internal_event_id:
613                     new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.split(' ')[0].replace('-', '')
614                 elif att.event_id.recurrent_id_date and source_attendee_record.google_internal_event_id:
615                     new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.replace('-', '').replace(' ', 'T').replace(':', '') + 'Z'
616
617                 if new_google_internal_event_id:
618                     #TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE !
619                     try:
620                         st, response = self.update_recurrent_event_exclu(cr, uid, new_google_internal_event_id, source_attendee_record.google_internal_event_id, att.event_id, context=context)
621                         if status_response(st):
622                             att_obj.write(cr, uid, [att.id], {'google_internal_event_id': new_google_internal_event_id}, context=context)
623                             new_ids.append(new_google_internal_event_id)
624                             cr.commit()
625                         else:
626                             _logger.warning("Impossible to create event %s. [%s]" % (att.event_id.id, st))
627                             _logger.warning("Response : %s" % response)
628                     except:
629                         pass
630         return new_ids
631
632     def update_events(self, cr, uid, lastSync=False, context=None):
633         if context is None:
634             context = {}
635
636         calendar_event = self.pool['calendar.event']
637         user_obj = self.pool['res.users']
638         att_obj = self.pool['calendar.attendee']
639         myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
640         context_novirtual = self.get_context_no_virtual(context)
641
642         if lastSync:
643             try:
644                 all_event_from_google = self.get_event_synchro_dict(cr, uid, lastSync=lastSync, context=context)
645             except urllib2.HTTPError, e:
646                 if e.code == 410:  # GONE, Google is lost.
647                     # 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.
648                     cr.rollback()
649                     registry = openerp.modules.registry.RegistryManager.get(request.session.db)
650                     with registry.cursor() as cur:
651                         self.pool['res.users'].write(cur, uid, [uid], {'google_calendar_last_sync_date': False}, context=context)
652                 error_key = simplejson.loads(e.read())
653                 error_key = error_key.get('error', {}).get('message', 'nc')
654                 error_msg = "Google are lost... the next synchro will be a full synchro. \n\n %s" % error_key
655                 raise self.pool.get('res.config.settings').get_config_warning(cr, _(error_msg), context=context)
656
657             my_google_att_ids = att_obj.search(cr, uid, [
658                 ('partner_id', '=', myPartnerID),
659                 ('google_internal_event_id', 'in', all_event_from_google.keys())
660             ], context=context_novirtual)
661
662             my_openerp_att_ids = att_obj.search(cr, uid, [
663                 ('partner_id', '=', myPartnerID),
664                 ('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)),
665                 ('google_internal_event_id', '!=', False),
666             ], context=context_novirtual)
667
668             my_openerp_googleinternal_ids = att_obj.read(cr, uid, my_openerp_att_ids, ['google_internal_event_id', 'event_id'], context=context_novirtual)
669
670             if self.get_print_log(cr, uid, context=context):
671                 _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))
672
673             for giid in my_openerp_googleinternal_ids:
674                 active = True  # if not sure, we request google
675                 if giid.get('event_id'):
676                     active = calendar_event.browse(cr, uid, int(giid.get('event_id')[0]), context=context_novirtual).active
677
678                 if giid.get('google_internal_event_id') and not all_event_from_google.get(giid.get('google_internal_event_id')) and active:
679                     one_event = self.get_one_event_synchro(cr, uid, giid.get('google_internal_event_id'), context=context)
680                     if one_event:
681                         all_event_from_google[one_event['id']] = one_event
682
683             my_att_ids = list(set(my_google_att_ids + my_openerp_att_ids))
684
685         else:
686             domain = [
687                 ('partner_id', '=', myPartnerID),
688                 ('google_internal_event_id', '!=', False),
689                 '|',
690                 ('event_id.stop', '>', self.get_minTime(cr, uid, context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
691                 ('event_id.final_date', '>', self.get_minTime(cr, uid, context).strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
692             ]
693
694             # Select all events from OpenERP which have been already synchronized in gmail
695             my_att_ids = att_obj.search(cr, uid, domain, context=context_novirtual)
696             all_event_from_google = self.get_event_synchro_dict(cr, uid, lastSync=False, context=context)
697
698         event_to_synchronize = {}
699         for att in att_obj.browse(cr, uid, my_att_ids, context=context):
700             event = att.event_id
701
702             base_event_id = att.google_internal_event_id.rsplit('_', 1)[0]
703
704             if base_event_id not in event_to_synchronize:
705                 event_to_synchronize[base_event_id] = {}
706
707             if att.google_internal_event_id not in event_to_synchronize[base_event_id]:
708                 event_to_synchronize[base_event_id][att.google_internal_event_id] = SyncEvent()
709
710             ev_to_sync = event_to_synchronize[base_event_id][att.google_internal_event_id]
711
712             ev_to_sync.OE.attendee_id = att.id
713             ev_to_sync.OE.event = event
714             ev_to_sync.OE.found = True
715             ev_to_sync.OE.event_id = event.id
716             ev_to_sync.OE.isRecurrence = event.recurrency
717             ev_to_sync.OE.isInstance = bool(event.recurrent_id and event.recurrent_id > 0)
718             ev_to_sync.OE.update = event.oe_update_date
719             ev_to_sync.OE.status = event.active
720             ev_to_sync.OE.synchro = att.oe_synchro_date
721
722         for event in all_event_from_google.values():
723             event_id = event.get('id')
724             base_event_id = event_id.rsplit('_', 1)[0]
725
726             if base_event_id not in event_to_synchronize:
727                 event_to_synchronize[base_event_id] = {}
728
729             if event_id not in event_to_synchronize[base_event_id]:
730                 event_to_synchronize[base_event_id][event_id] = SyncEvent()
731
732             ev_to_sync = event_to_synchronize[base_event_id][event_id]
733
734             ev_to_sync.GG.event = event
735             ev_to_sync.GG.found = True
736             ev_to_sync.GG.isRecurrence = bool(event.get('recurrence', ''))
737             ev_to_sync.GG.isInstance = bool(event.get('recurringEventId', 0))
738             ev_to_sync.GG.update = event.get('updated', None)  # if deleted, no date without browse event
739             if ev_to_sync.GG.update:
740                 ev_to_sync.GG.update = ev_to_sync.GG.update.replace('T', ' ').replace('Z', '')
741             ev_to_sync.GG.status = (event.get('status') != 'cancelled')
742
743         ######################
744         #   PRE-PROCESSING   #
745         ######################
746         for base_event in event_to_synchronize:
747             for current_event in event_to_synchronize[base_event]:
748                 event_to_synchronize[base_event][current_event].compute_OP(modeFull=not lastSync)
749             if self.get_print_log(cr, uid, context=context):
750                 if not isinstance(event_to_synchronize[base_event][current_event].OP, NothingToDo):
751                     _logger.info(event_to_synchronize[base_event])
752
753         ######################
754         #      DO ACTION     #
755         ######################
756         for base_event in event_to_synchronize:
757             event_to_synchronize[base_event] = sorted(event_to_synchronize[base_event].iteritems(), key=operator.itemgetter(0))
758             for current_event in event_to_synchronize[base_event]:
759                 cr.commit()
760                 event = current_event[1]  # event is an Sync Event !
761                 actToDo = event.OP
762                 actSrc = event.OP.src
763
764                 context['curr_attendee'] = event.OE.attendee_id
765
766                 if isinstance(actToDo, NothingToDo):
767                     continue
768                 elif isinstance(actToDo, Create):
769                     context_tmp = context.copy()
770                     context_tmp['NewMeeting'] = True
771                     if actSrc == 'GG':
772                         res = self.update_from_google(cr, uid, False, event.GG.event, "create", context=context_tmp)
773                         event.OE.event_id = res
774                         meeting = calendar_event.browse(cr, uid, res, context=context)
775                         attendee_record_id = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('event_id', '=', res)], context=context)
776                         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)
777                     elif actSrc == 'OE':
778                         raise "Should be never here, creation for OE is done before update !"
779                     #TODO Add to batch
780                 elif isinstance(actToDo, Update):
781                     if actSrc == 'GG':
782                         self.update_from_google(cr, uid, event.OE.event, event.GG.event, 'write', context)
783                     elif actSrc == 'OE':
784                         self.update_to_google(cr, uid, event.OE.event, event.GG.event, context)
785                 elif isinstance(actToDo, Exclude):
786                     if actSrc == 'OE':
787                         self.delete_an_event(cr, uid, current_event[0], context=context)
788                     elif actSrc == 'GG':
789                         new_google_event_id = event.GG.event['id'].rsplit('_', 1)[1]
790                         if 'T' in new_google_event_id:
791                             new_google_event_id = new_google_event_id.replace('T', '')[:-1]
792                         else:
793                             new_google_event_id = new_google_event_id + "000000"
794
795                         if event.GG.status:
796                             parent_event = {}
797                             if not event_to_synchronize[base_event][0][1].OE.event_id:
798                                 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)
799                                 event_to_synchronize[base_event][0][1].OE.event_id = main_ev[0].get('event_id')[0]
800
801                             parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
802                             res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context)
803                         else:
804                             parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id
805                             calendar_event.unlink(cr, uid, "%s-%s" % (parent_oe_id, new_google_event_id), can_be_deleted=True, context=context)
806
807                 elif isinstance(actToDo, Delete):
808                     if actSrc == 'GG':
809                         try:
810                             self.delete_an_event(cr, uid, current_event[0], context=context)
811                         except Exception, e:
812                             error = simplejson.loads(e.read())
813                             error_nr = error.get('error', {}).get('code')
814                             # if already deleted from gmail or never created
815                             if error_nr in (404, 410,):
816                                 pass
817                             else:
818                                 raise e
819                     elif actSrc == 'OE':
820                         calendar_event.unlink(cr, uid, event.OE.event_id, can_be_deleted=False, context=context)
821         return True
822
823     def check_and_sync(self, cr, uid, oe_event, google_event, context):
824         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"):
825             self.update_to_google(cr, uid, oe_event, google_event, context)
826         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"):
827             self.update_from_google(cr, uid, oe_event, google_event, 'write', context)
828
829     def get_sequence(self, cr, uid, instance_id, context=None):
830         gs_pool = self.pool['google.service']
831
832         params = {
833             'fields': 'sequence',
834             'access_token': self.get_token(cr, uid, context)
835         }
836
837         headers = {'Content-type': 'application/json'}
838
839         url = "/calendar/v3/calendars/%s/events/%s" % ('primary', instance_id)
840
841         st, content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context)
842         return content.get('sequence', 0)
843 #################################
844 ##  MANAGE CONNEXION TO GMAIL  ##
845 #################################
846
847     def get_token(self, cr, uid, context=None):
848         current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
849         if not current_user.google_calendar_token_validity or \
850                 datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) < (datetime.now() + timedelta(minutes=1)):
851             self.do_refresh_token(cr, uid, context=context)
852             current_user.refresh()
853         return current_user.google_calendar_token
854
855     def get_last_sync_date(self, cr, uid, context=None):
856         current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
857         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
858
859     def do_refresh_token(self, cr, uid, context=None):
860         current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
861         gs_pool = self.pool['google.service']
862
863         all_token = gs_pool._refresh_google_token_json(cr, uid, current_user.google_calendar_rtoken, self.STR_SERVICE, context=context)
864
865         vals = {}
866         vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in'))
867         vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token')
868
869         self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context)
870
871     def need_authorize(self, cr, uid, context=None):
872         current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
873         return current_user.google_calendar_rtoken is False
874
875     def get_calendar_scope(self, RO=False):
876         readonly = RO and '.readonly' or ''
877         return 'https://www.googleapis.com/auth/calendar%s' % (readonly)
878
879     def authorize_google_uri(self, cr, uid, from_url='http://www.openerp.com', context=None):
880         url = self.pool['google.service']._get_authorize_uri(cr, uid, from_url, self.STR_SERVICE, scope=self.get_calendar_scope(), context=context)
881         return url
882
883     def can_authorize_google(self, cr, uid, context=None):
884         return self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager')
885
886     def set_all_tokens(self, cr, uid, authorization_code, context=None):
887         gs_pool = self.pool['google.service']
888         all_token = gs_pool._get_google_token_json(cr, uid, authorization_code, self.STR_SERVICE, context=context)
889
890         vals = {}
891         vals['google_%s_rtoken' % self.STR_SERVICE] = all_token.get('refresh_token')
892         vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in'))
893         vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token')
894         self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context)
895
896     def get_minTime(self, cr, uid, context=None):
897         number_of_week = self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.week_synchro', default=13)
898         return datetime.now() - timedelta(weeks=number_of_week)
899
900     def get_need_synchro_attendee(self, cr, uid, context=None):
901         return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_synchro_attendee', default=True)
902
903     def get_disable_since_synchro(self, cr, uid, context=None):
904         return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_since_synchro', default=False)
905
906     def get_print_log(self, cr, uid, context=None):
907         return self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.debug_print', default=False)
908
909
910 class res_users(osv.Model):
911     _inherit = 'res.users'
912
913     _columns = {
914         'google_calendar_rtoken': fields.char('Refresh Token'),
915         'google_calendar_token': fields.char('User token'),
916         'google_calendar_token_validity': fields.datetime('Token Validity'),
917         'google_calendar_last_sync_date': fields.datetime('Last synchro date'),
918         'google_calendar_cal_id': fields.char('Calendar ID', help='Last Calendar ID who has been synchronized. If it is changed, we remove \
919 all links between GoogleID and OpenERP Google Internal ID')
920     }
921
922
923 class calendar_event(osv.Model):
924     _inherit = "calendar.event"
925
926     def get_fields_need_update_google(self, cr, uid, context=None):
927         return ['name', 'description', 'allday', 'date', 'date_end', 'stop', 'attendee_ids', 'location', 'class', 'active']
928
929     def write(self, cr, uid, ids, vals, context=None):
930         if context is None:
931             context = {}
932         sync_fields = set(self.get_fields_need_update_google(cr, uid, context))
933         if (set(vals.keys()) & sync_fields) and 'oe_update_date' not in vals.keys() and 'NewMeeting' not in context:
934             vals['oe_update_date'] = datetime.now()
935
936         return super(calendar_event, self).write(cr, uid, ids, vals, context=context)
937
938     def copy(self, cr, uid, id, default=None, context=None):
939         default = default or {}
940         default['attendee_ids'] = False
941         if default.get('write_type', False):
942             del default['write_type']
943         elif default.get('recurrent_id', False):
944             default['oe_update_date'] = datetime.now()
945         else:
946             default['oe_update_date'] = False
947         return super(calendar_event, self).copy(cr, uid, id, default, context)
948
949     def unlink(self, cr, uid, ids, can_be_deleted=False, context=None):
950         return super(calendar_event, self).unlink(cr, uid, ids, can_be_deleted=can_be_deleted, context=context)
951
952     _columns = {
953         'oe_update_date': fields.datetime('OpenERP Update Date'),
954     }
955
956
957 class calendar_attendee(osv.Model):
958     _inherit = 'calendar.attendee'
959
960     _columns = {
961         'google_internal_event_id': fields.char('Google Calendar Event Id'),
962         'oe_synchro_date': fields.datetime('OpenERP Synchro Date'),
963     }
964     _sql_constraints = [('google_id_uniq', 'unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!')]
965
966     def write(self, cr, uid, ids, vals, context=None):
967         if context is None:
968             context = {}
969
970         for id in ids:
971             ref = vals.get('event_id', self.browse(cr, uid, id, context=context).event_id.id)
972
973             # If attendees are updated, we need to specify that next synchro need an action
974             # Except if it come from an update_from_google
975             if not context.get('curr_attendee', False) and not context.get('NewMeeting', False):
976                 self.pool['calendar.event'].write(cr, uid, ref, {'oe_update_date': datetime.now()}, context)
977         return super(calendar_attendee, self).write(cr, uid, ids, vals, context=context)