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