[MERGE] forward port of branch saas-3 up to bf53aed
[odoo/odoo.git] / addons / google_calendar / google_calendar.py
1 ##############################################################################
2 #
3 #    OpenERP, Open Source Management Solution
4 #    Copyright (C) 2004-2012 OpenERP SA (<http://www.openerp.com>).
5 #
6 #    This program is free software: you can redistribute it and/or modify
7 #    it under the terms of the GNU Affero General Public License as
8 #    published by the Free Software Foundation, either version 3 of the
9 #    License, or (at your option) any later version.
10
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import operator
23 import simplejson
24 import urllib
25
26 from openerp import tools
27 from openerp import SUPERUSER_ID
28
29 from datetime import datetime, timedelta
30 from dateutil import parser
31 import pytz
32 from openerp.osv import fields, osv
33
34 import logging
35 _logger = logging.getLogger(__name__)
36
37
38 class Meta(type):
39     """ This Meta class allow to define class as a structure, and so instancied variable
40         in __init__ to avoid to have side effect alike 'static' variable """
41     def __new__(typ, name, parents, attrs):
42         methods = dict((k, v) for k, v in attrs.iteritems()
43                        if callable(v))
44         attrs = dict((k, v) for k, v in attrs.iteritems()
45                      if not callable(v))
46
47         def init(self, **kw):
48             for k, v in attrs.iteritems():
49                 setattr(self, k, v)
50             for k, v in kw.iteritems():
51                 assert k in attrs
52                 setattr(self, k, v)
53
54         methods['__init__'] = init
55         methods['__getitem__'] = getattr
56         return type.__new__(typ, name, parents, methods)
57
58
59 class Struct(object):
60     __metaclass__ = Meta
61
62
63 class OpenerpEvent(Struct):
64         event = False
65         found = False
66         event_id = False
67         isRecurrence = False
68         isInstance = False
69         update = False
70         status = False
71         attendee_id = False
72         synchro = False
73
74
75 class GmailEvent(Struct):
76     event = False
77     found = False
78     isRecurrence = False
79     isInstance = False
80     update = False
81     status = False
82
83
84 class SyncEvent(object):
85     def __init__(self):
86         self.OE = OpenerpEvent()
87         self.GG = GmailEvent()
88         self.OP = None
89
90     def __getitem__(self, key):
91         return getattr(self, key)
92
93     def compute_OP(self):
94         #If event are already in Gmail and in OpenERP
95         if self.OE.found and self.GG.found:
96             #If the event has been deleted from one side, we delete on other side !
97             if self.OE.status != self.GG.status:
98                 self.OP = Delete((self.OE.status and "OE") or (self.GG.status and "GG"),
99                                  'The event has been deleted from one side, we delete on other side !')
100             #If event is not deleted !
101             elif self.OE.status and self.GG.status:
102                 if self.OE.update.split('.')[0] != self.GG.update.split('.')[0]:
103                     if self.OE.update < self.GG.update:
104                         tmpSrc = 'GG'
105                     elif self.OE.update > self.GG.update:
106                         tmpSrc = 'OE'
107                     assert tmpSrc in ['GG', 'OE']
108
109                     #if self.OP.action == None:
110                     if self[tmpSrc].isRecurrence:
111                         if self[tmpSrc].status:
112                             self.OP = Update(tmpSrc, 'Only need to update, because i\'m active')
113                         else:
114                             self.OP = Exclude(tmpSrc, 'Need to Exclude (Me = First event from recurrence) from recurrence')
115
116                     elif self[tmpSrc].isInstance:
117                         self.OP = Update(tmpSrc, 'Only need to update, because already an exclu')
118                     else:
119                         self.OP = Update(tmpSrc, 'Simply Update... I\'m a single event')
120                 else:
121                     if not self.OE.synchro or self.OE.synchro.split('.')[0] < self.OE.update.split('.')[0]:
122                         self.OP = Update('OE', 'Event already updated by another user, but not synchro with my google calendar')
123                         #import ipdb; ipdb.set_trace();
124                     else:
125                         self.OP = NothingToDo("", 'Not update needed')
126             else:
127                 self.OP = NothingToDo("", "Both are already deleted")
128
129         # New in openERP...  Create on create_events of synchronize function
130         elif self.OE.found and not self.GG.found:
131             #Has been deleted from gmail
132             if self.OE.status:
133                 self.OP = Delete('OE', 'Removed from GOOGLE')
134             else:
135                 self.OP = NothingToDo("", "Already Deleted in gmail and unlinked in OpenERP")
136         elif self.GG.found and not self.OE.found:
137             tmpSrc = 'GG'
138             if not self.GG.status and not self.GG.isInstance:
139                 # don't need to make something... because event has been created and deleted before the synchronization
140                 self.OP = NothingToDo("", 'Nothing to do... Create and Delete directly')
141             else:
142                 if self.GG.isInstance:
143                     if self[tmpSrc].status:
144                         self.OP = Exclude(tmpSrc, 'Need to create the new exclu')
145                     else:
146                         self.OP = Exclude(tmpSrc, 'Need to copy and Exclude')
147                 else:
148                     self.OP = Create(tmpSrc, 'New EVENT CREATE from GMAIL')
149
150     def __str__(self):
151         return self.__repr__()
152
153     def __repr__(self):
154         myPrint = "---- A SYNC EVENT ---"
155         myPrint += "\n    ID          OE: %s " % (self.OE.event and self.OE.event.id)
156         myPrint += "\n    ID          GG: %s " % (self.GG.event and self.GG.event.get('id', False))
157         myPrint += "\n    Name        OE: %s " % (self.OE.event and self.OE.event.name)
158         myPrint += "\n    Name        GG: %s " % (self.GG.event and self.GG.event.get('summary', False))
159         myPrint += "\n    Found       OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found)
160         myPrint += "\n    Recurrence  OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence)
161         myPrint += "\n    Instance    OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance)
162         myPrint += "\n    Synchro     OE: %10s " % (self.OE.synchro)
163         myPrint += "\n    Update      OE: %10s " % (self.OE.update)
164         myPrint += "\n    Update      GG: %10s " % (self.GG.update)
165         myPrint += "\n    Status      OE:%5s vs GG: %5s" % (self.OE.status, self.GG.status)
166         if (self.OP is None):
167             myPrint += "\n    Action      %s" % "---!!!---NONE---!!!---"
168         else:
169             myPrint += "\n    Action      %s" % type(self.OP).__name__
170             myPrint += "\n    Source      %s" % (self.OP.src)
171             myPrint += "\n    comment     %s" % (self.OP.info)
172         return myPrint
173
174
175 class SyncOperation(object):
176     def __init__(self, src, info, **kw):
177         self.src = src
178         self.info = info
179         for k, v in kw.items():
180             setattr(self, k, v)
181
182     def __str__(self):
183         return 'in__STR__'
184
185
186 class Create(SyncOperation):
187     pass
188
189
190 class Update(SyncOperation):
191     pass
192
193
194 class Delete(SyncOperation):
195     pass
196
197
198 class NothingToDo(SyncOperation):
199     pass
200
201
202 class Exclude(SyncOperation):
203     pass
204
205
206 class google_calendar(osv.AbstractModel):
207     STR_SERVICE = 'calendar'
208     _name = 'google.%s' % STR_SERVICE
209
210     def generate_data(self, cr, uid, event, context=None):
211         if event.allday:
212             start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T').split('T')[0]
213             end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=event.duration), context=context).isoformat('T').split('T')[0]
214             type = 'date'
215             vstype = 'dateTime'
216         else:
217             start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T')
218             end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T')
219             type = 'dateTime'
220             vstype = 'date'
221         attendee_list = []
222
223         for attendee in event.attendee_ids:
224             attendee_list.append({
225                 'email': attendee.email or 'NoEmail@mail.com',
226                 'displayName': attendee.partner_id.name,
227                 'responseStatus': attendee.state or 'needsAction',
228             })
229         data = {
230             "summary": event.name or '',
231             "description": event.description or '',
232             "start": {
233                 type: start_date,
234                 vstype: None,
235                 'timeZone': 'UTC'
236             },
237             "end": {
238                 type: end_date,
239                 vstype: None,
240                 'timeZone': 'UTC'
241             },
242             "attendees": attendee_list,
243             "location": event.location or '',
244             "visibility": event['class'] or 'public',
245         }
246         if event.recurrency and event.rrule:
247             data["recurrence"] = ["RRULE:" + event.rrule]
248
249         if not event.active:
250             data["state"] = "cancelled"
251
252         if not self.get_need_synchro_attendee(cr, uid, context=context):
253             data.pop("attendees")
254
255         return data
256
257     def create_an_event(self, cr, uid, event, context=None):
258         gs_pool = self.pool['google.service']
259
260         data = self.generate_data(cr, uid, event, context=context)
261
262         url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urllib.quote('id,updated'), self.get_token(cr, uid, context))
263         headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
264         data_json = simplejson.dumps(data)
265
266         return gs_pool._do_request(cr, uid, url, data_json, headers, type='POST', context=context)
267
268     def delete_an_event(self, cr, uid, event_id, context=None):
269         gs_pool = self.pool['google.service']
270
271         params = {
272             'access_token': self.get_token(cr, uid, context)
273         }
274         headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
275         url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event_id)
276
277         return gs_pool._do_request(cr, uid, url, params, headers, type='DELETE', context=context)
278
279     def get_event_dict(self, cr, uid, token=False, nextPageToken=False, context=None):
280         if not token:
281             token = self.get_token(cr, uid, context)
282
283         gs_pool = self.pool['google.service']
284
285         params = {
286             'fields': 'items,nextPageToken',
287             'access_token': token,
288             'maxResults': 1000,
289             'timeMin': self.get_start_time_to_synchro(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz"),
290         }
291         headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
292
293         url = "/calendar/v3/calendars/%s/events" % 'primary'
294         if nextPageToken:
295             params['pageToken'] = nextPageToken
296
297         content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context)
298
299         google_events_dict = {}
300
301         for google_event in content['items']:
302             google_events_dict[google_event['id']] = google_event
303
304         if content.get('nextPageToken', False):
305             google_events_dict.update(self.get_event_dict(cr, uid, token, content['nextPageToken'], context=context))
306         return google_events_dict
307
308     def update_to_google(self, cr, uid, oe_event, google_event, context):
309         calendar_event = self.pool['calendar.event']
310         gs_pool = self.pool['google.service']
311
312         url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'], 'id,updated', self.get_token(cr, uid, context))
313         headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
314         data = self.generate_data(cr, uid, oe_event, context)
315         data['sequence'] = google_event.get('sequence', 0)
316         data_json = simplejson.dumps(data)
317
318         content = gs_pool._do_request(cr, uid, url, data_json, headers, type='PATCH', context=context)
319
320         update_date = datetime.strptime(content['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
321         calendar_event.write(cr, uid, [oe_event.id], {'oe_update_date': update_date})
322
323         if context['curr_attendee']:
324             self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date}, context)
325
326     def update_an_event(self, cr, uid, event, context=None):
327         gs_pool = self.pool['google.service']
328
329         data = self.generate_data(cr, uid, event, context=context)
330
331         url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event.google_internal_event_id)
332         headers = {}
333         data['access_token'] = self.get_token(cr, uid, context)
334
335         response = gs_pool._do_request(cr, uid, url, data, headers, type='GET', context=context)
336         #TO_CHECK : , if http fail, no event, do DELETE ?
337         return response
338
339     def update_recurrent_event_exclu(self, cr, uid, instance_id, event_ori_google_id, event_new, context=None):
340         gs_pool = self.pool['google.service']
341
342         data = self.generate_data(cr, uid, event_new, context=context)
343
344         data['recurringEventId'] = event_ori_google_id
345         data['originalStartTime'] = event_new.recurrent_id_date
346
347         url = "/calendar/v3/calendars/%s/events/%s?access_token=%s" % ('primary', instance_id, self.get_token(cr, uid, context))
348         headers = {'Content-type': 'application/json'}
349
350         data['sequence'] = self.get_sequence(cr, uid, instance_id, context)
351
352         data_json = simplejson.dumps(data)
353         return gs_pool._do_request(cr, uid, url, data_json, headers, type='PUT', context=context)
354
355     def update_from_google(self, cr, uid, event, single_event_dict, type, context):
356         if context is None:
357             context = []
358
359         calendar_event = self.pool['calendar.event']
360         res_partner_obj = self.pool['res.partner']
361         calendar_attendee_obj = self.pool['calendar.attendee']
362         user_obj = self.pool['res.users']
363         myPartnerID = user_obj.browse(cr, uid, uid, context).partner_id.id
364         attendee_record = []
365         partner_record = [(4, myPartnerID)]
366         result = {}
367
368         if single_event_dict.get('attendees', False):
369             for google_attendee in single_event_dict['attendees']:
370                 if type == "write":
371                     for oe_attendee in event['attendee_ids']:
372                         if oe_attendee.email == google_attendee['email']:
373                             calendar_attendee_obj.write(cr, uid, [oe_attendee.id], {'state': google_attendee['responseStatus']}, context=context)
374                             google_attendee['found'] = True
375                             continue
376
377                 if google_attendee.get('found', False):
378                     continue
379                 if self.get_need_synchro_attendee(cr, uid, context=context):
380                     attendee_id = res_partner_obj.search(cr, uid, [('email', '=', google_attendee['email'])], context=context)
381                     if not attendee_id:
382                         attendee_id = [res_partner_obj.create(cr, uid, {'email': google_attendee['email'], 'customer': False, 'name': google_attendee.get("displayName", False) or google_attendee['email']}, context=context)]
383                     attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context)
384                     partner_record.append((4, attendee.get('id')))
385                     attendee['partner_id'] = attendee.pop('id')
386                     attendee['state'] = google_attendee['responseStatus']
387                     attendee_record.append((0, 0, attendee))
388
389         UTC = pytz.timezone('UTC')
390         if single_event_dict.get('start') and single_event_dict.get('end'):  # If not cancelled
391             if single_event_dict['start'].get('dateTime', False) and single_event_dict['end'].get('dateTime', False):
392                 date = parser.parse(single_event_dict['start']['dateTime'])
393                 date_deadline = parser.parse(single_event_dict['end']['dateTime'])
394                 delta = date_deadline.astimezone(UTC) - date.astimezone(UTC)
395                 date = str(date.astimezone(UTC))[:-6]
396                 date_deadline = str(date_deadline.astimezone(UTC))[:-6]
397                 allday = False
398             else:
399                 date = (single_event_dict['start']['date'] + ' 00:00:00')
400                 date_deadline = (single_event_dict['end']['date'] + ' 00:00:00')
401                 d_start = datetime.strptime(date, "%Y-%m-%d %H:%M:%S")
402                 d_end = datetime.strptime(date_deadline, "%Y-%m-%d %H:%M:%S")
403                 delta = (d_end - d_start)
404                 allday = True
405
406             result['duration'] = (delta.seconds / 60) / 60.0 + delta.days * 24
407             update_date = datetime.strptime(single_event_dict['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
408             result.update({
409                 'date': date,
410                 'date_deadline': date_deadline,
411                 'allday': allday
412             })
413         result.update({
414             'attendee_ids': attendee_record,
415             'partner_ids': list(set(partner_record)),
416
417             'name': single_event_dict.get('summary', 'Event'),
418             'description': single_event_dict.get('description', False),
419             'location': single_event_dict.get('location', False),
420             'class': single_event_dict.get('visibility', 'public'),
421             'oe_update_date': update_date,
422             # 'google_internal_event_id': single_event_dict.get('id',False),
423         })
424
425         if single_event_dict.get("recurrence", False):
426             rrule = [rule for rule in single_event_dict["recurrence"] if rule.startswith("RRULE:")][0][6:]
427             result['rrule'] = rrule
428
429         if type == "write":
430             res = calendar_event.write(cr, uid, event['id'], result, context=context)
431         elif type == "copy":
432             result['recurrence'] = True
433             res = calendar_event.write(cr, uid, [event['id']], result, context=context)
434
435         elif type == "create":
436             res = calendar_event.create(cr, uid, result, context=context)
437
438         if context['curr_attendee']:
439             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)
440         return res
441
442     def synchronize_events(self, cr, uid, ids, context=None):
443         # Create all new events from OpenERP into Gmail, if that is not recurrent event
444         self.create_new_events(cr, uid, context=context)
445         self.bind_recurring_events_to_google(cr, uid, context)
446         res = self.update_events(cr, uid, context)
447
448         return {
449             "status": res and "need_refresh" or "no_new_event_form_google",
450             "url": ''
451         }
452
453     def create_new_events(self, cr, uid, context=None):
454         ev_obj = self.pool['calendar.event']
455         att_obj = self.pool['calendar.attendee']
456         user_obj = self.pool['res.users']
457         myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
458
459         context_norecurrent = context.copy()
460         context_norecurrent['virtual_id'] = False
461
462         my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID),
463                                     ('google_internal_event_id', '=', False),
464                                     '|',
465                                     ('event_id.date_deadline', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
466                                     ('event_id.end_date', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
467                                     ], context=context_norecurrent)
468
469         for att in att_obj.browse(cr, uid, my_att_ids, context=context):
470             if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0:
471                 response = self.create_an_event(cr, uid, att.event_id, context=context)
472                 update_date = datetime.strptime(response['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
473                 ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date': update_date})
474                 att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date': update_date})
475                 cr.commit()
476
477     def bind_recurring_events_to_google(self, cr, uid, context):
478         ev_obj = self.pool['calendar.event']
479         att_obj = self.pool['calendar.attendee']
480         user_obj = self.pool['res.users']
481         myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
482
483         context_norecurrent = context.copy()
484         context_norecurrent['virtual_id'] = False
485         context_norecurrent['active_test'] = False
486
487         my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('google_internal_event_id', '=', False)], context=context_norecurrent)
488
489         for att in att_obj.browse(cr, uid, my_att_ids, context=context):
490             if att.event_id.recurrent_id and att.event_id.recurrent_id > 0:
491                 new_google_internal_event_id = False
492                 source_event_record = ev_obj.browse(cr, uid, att.event_id.recurrent_id, context)
493                 source_attendee_record_id = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('event_id', '=', source_event_record.id)], context=context)
494                 source_attendee_record = att_obj.browse(cr, uid, source_attendee_record_id, context)[0]
495
496                 if att.event_id.recurrent_id_date and source_event_record.allday and source_attendee_record.google_internal_event_id:
497                     new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.split(' ')[0].replace('-', '')
498                 elif att.event_id.recurrent_id_date and source_attendee_record.google_internal_event_id:
499                     new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.replace('-', '').replace(' ', 'T').replace(':', '') + 'Z'
500
501                 if new_google_internal_event_id:
502                     #TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE !
503                     self.update_recurrent_event_exclu(cr, uid, new_google_internal_event_id, source_attendee_record.google_internal_event_id, att.event_id, context=context)
504                     att_obj.write(cr, uid, [att.id], {'google_internal_event_id': new_google_internal_event_id}, context=context)
505                     cr.commit()
506
507     def update_events(self, cr, uid, context=None):
508         if context is None:
509             context = {}
510
511         calendar_event = self.pool['calendar.event']
512         user_obj = self.pool['res.users']
513         att_obj = self.pool['calendar.attendee']
514         myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
515
516         context_novirtual = context.copy()
517         context_novirtual['virtual_id'] = False
518         context_novirtual['active_test'] = False
519
520         all_event_from_google = self.get_event_dict(cr, uid, context=context)
521
522         # Select all events from OpenERP which have been already synchronized in gmail
523         my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID),
524                                     ('google_internal_event_id', '!=', False),
525                                     '|',
526                                     ('event_id.date_deadline', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
527                                     ('event_id.end_date', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
528                                     ], context=context_novirtual)
529         event_to_synchronize = {}
530         for att in att_obj.browse(cr, uid, my_att_ids, context=context):
531             event = att.event_id
532
533             base_event_id = att.google_internal_event_id.rsplit('_', 1)[0]
534
535             if base_event_id not in event_to_synchronize:
536                 event_to_synchronize[base_event_id] = {}
537
538             if att.google_internal_event_id not in event_to_synchronize[base_event_id]:
539                 event_to_synchronize[base_event_id][att.google_internal_event_id] = SyncEvent()
540
541             ev_to_sync = event_to_synchronize[base_event_id][att.google_internal_event_id]
542
543             ev_to_sync.OE.attendee_id = att.id
544             ev_to_sync.OE.event = event
545             ev_to_sync.OE.found = True
546             ev_to_sync.OE.event_id = event.id
547             ev_to_sync.OE.isRecurrence = event.recurrency
548             ev_to_sync.OE.isInstance = bool(event.recurrent_id and event.recurrent_id > 0)
549             ev_to_sync.OE.update = event.oe_update_date
550             ev_to_sync.OE.status = event.active
551             ev_to_sync.OE.synchro = att.oe_synchro_date
552
553         for event in all_event_from_google.values():
554             event_id = event.get('id')
555             base_event_id = event_id.rsplit('_', 1)[0]
556
557             if base_event_id not in event_to_synchronize:
558                 event_to_synchronize[base_event_id] = {}
559
560             if event_id not in event_to_synchronize[base_event_id]:
561                 event_to_synchronize[base_event_id][event_id] = SyncEvent()
562
563             ev_to_sync = event_to_synchronize[base_event_id][event_id]
564
565             ev_to_sync.GG.event = event
566             ev_to_sync.GG.found = True
567             ev_to_sync.GG.isRecurrence = bool(event.get('recurrence', ''))
568             ev_to_sync.GG.isInstance = bool(event.get('recurringEventId', 0))
569             ev_to_sync.GG.update = event.get('updated', None)  # if deleted, no date without browse event
570             if ev_to_sync.GG.update:
571                 ev_to_sync.GG.update = ev_to_sync.GG.update.replace('T', ' ').replace('Z', '')
572             ev_to_sync.GG.status = (event.get('status') != 'cancelled')
573
574         ######################
575         #   PRE-PROCESSING   #
576         ######################
577         for base_event in event_to_synchronize:
578             for current_event in event_to_synchronize[base_event]:
579                 event_to_synchronize[base_event][current_event].compute_OP()
580             #print event_to_synchronize[base_event]
581
582         ######################
583         #      DO ACTION     #
584         ######################
585         for base_event in event_to_synchronize:
586             event_to_synchronize[base_event] = sorted(event_to_synchronize[base_event].iteritems(), key=operator.itemgetter(0))
587             for current_event in event_to_synchronize[base_event]:
588                 cr.commit()
589                 event = current_event[1]  # event is an Sync Event !
590                 actToDo = event.OP
591                 actSrc = event.OP.src
592
593                 context['curr_attendee'] = event.OE.attendee_id
594
595                 if isinstance(actToDo, NothingToDo):
596                     continue
597                 elif isinstance(actToDo, Create):
598                     context_tmp = context.copy()
599                     context_tmp['NewMeeting'] = True
600                     if actSrc == 'GG':
601                         res = self.update_from_google(cr, uid, False, event.GG.event, "create", context=context_tmp)
602                         event.OE.event_id = res
603                         meeting = calendar_event.browse(cr, uid, res, context=context)
604                         attendee_record_id = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('event_id', '=', res)], context=context)
605                         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)
606                     elif actSrc == 'OE':
607                         raise "Should be never here, creation for OE is done before update !"
608                     #TODO Add to batch
609                 elif isinstance(actToDo, Update):
610                     if actSrc == 'GG':
611                         self.update_from_google(cr, uid, event.OE.event, event.GG.event, 'write', context)
612                     elif actSrc == 'OE':
613                         self.update_to_google(cr, uid, event.OE.event, event.GG.event, context)
614                 elif isinstance(actToDo, Exclude):
615                     if actSrc == 'OE':
616                         self.delete_an_event(cr, uid, current_event[0], context=context)
617                     elif actSrc == 'GG':
618                             new_google_event_id = event.GG.event['id'].rsplit('_', 1)[1]
619                             if 'T' in new_google_event_id:
620                                 new_google_event_id = new_google_event_id.replace('T', '')[:-1]
621                             else:
622                                 new_google_event_id = new_google_event_id + "000000"
623
624                                 if event.GG.status:
625                                     parent_event = {}
626                                     if event_to_synchronize[base_event][0][1].OE.event_id:
627                                         parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
628                                     else:
629                                         main_ev = att_obj.search_read(cr, uid, [('google_internal_event_id', '=', event.GG.event['id'].rsplit('_', 1)[0])], fields=['event_id'], context=context)
630                                         parent_event['id'] = "%s-%s" % (main_ev[0].get('event_id')[0], new_google_event_id)
631                                     res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context)
632                                 else:
633                                     if event_to_synchronize[base_event][0][1].OE.event_id:
634                                         parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id
635                                         calendar_event.unlink(cr, uid, "%s-%s" % (parent_oe_id, new_google_event_id), unlink_level=1, context=context)
636
637                 elif isinstance(actToDo, Delete):
638                     if actSrc == 'GG':
639                         self.delete_an_event(cr, uid, current_event[0], context=context)
640                     elif actSrc == 'OE':
641                         calendar_event.unlink(cr, uid, event.OE.event_id, unlink_level=0, context=context)
642         return True
643
644     def check_and_sync(self, cr, uid, oe_event, google_event, context):
645         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"):
646             self.update_to_google(cr, uid, oe_event, google_event, context)
647         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"):
648             self.update_from_google(cr, uid, oe_event, google_event, 'write', context)
649
650     def get_sequence(self, cr, uid, instance_id, context=None):
651         gs_pool = self.pool['google.service']
652
653         params = {
654             'fields': 'sequence',
655             'access_token': self.get_token(cr, uid, context)
656         }
657
658         headers = {'Content-type': 'application/json'}
659
660         url = "/calendar/v3/calendars/%s/events/%s" % ('primary', instance_id)
661
662         content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context)
663         return content.get('sequence', 0)
664 #################################
665 ##  MANAGE CONNEXION TO GMAIL  ##
666 #################################
667
668     def get_token(self, cr, uid, context=None):
669         current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
670
671         if datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], "%Y-%m-%d %H:%M:%S") < (datetime.now() + timedelta(minutes=1)):
672             self.do_refresh_token(cr, uid, context=context)
673             current_user.refresh()
674
675         return current_user.google_calendar_token
676
677     def do_refresh_token(self, cr, uid, context=None):
678         current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
679         gs_pool = self.pool['google.service']
680
681         all_token = gs_pool._refresh_google_token_json(cr, uid, current_user.google_calendar_rtoken, self.STR_SERVICE, context=context)
682
683         vals = {}
684         vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in'))
685         vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token')
686
687         self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context)
688
689     def need_authorize(self, cr, uid, context=None):
690         current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
691         return current_user.google_calendar_rtoken is False
692
693     def get_calendar_scope(self, RO=False):
694         readonly = RO and '.readonly' or ''
695         return 'https://www.googleapis.com/auth/calendar%s' % (readonly)
696
697     def authorize_google_uri(self, cr, uid, from_url='http://www.openerp.com', context=None):
698         url = self.pool['google.service']._get_authorize_uri(cr, uid, from_url, self.STR_SERVICE, scope=self.get_calendar_scope(), context=context)
699         return url
700
701     def can_authorize_google(self, cr, uid, context=None):
702         return self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager')
703
704     def set_all_tokens(self, cr, uid, authorization_code, context=None):
705         gs_pool = self.pool['google.service']
706         all_token = gs_pool._get_google_token_json(cr, uid, authorization_code, self.STR_SERVICE, context=context)
707
708         vals = {}
709         vals['google_%s_rtoken' % self.STR_SERVICE] = all_token.get('refresh_token')
710         vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in'))
711         vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token')
712         self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context)
713
714     def get_start_time_to_synchro(self, cr, uid, context=None):
715         # WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4
716         number_of_week = 13
717         return datetime.now() - timedelta(weeks=number_of_week)
718
719     def get_need_synchro_attendee(self, cr, uid, context=None):
720         # WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4
721         return True
722
723
724 class res_users(osv.Model):
725     _inherit = 'res.users'
726
727     _columns = {
728         'google_calendar_rtoken': fields.char('Refresh Token'),
729         'google_calendar_token': fields.char('User token'),
730         'google_calendar_token_validity': fields.datetime('Token Validity'),
731     }
732
733
734 class calendar_event(osv.Model):
735     _inherit = "calendar.event"
736
737     def write(self, cr, uid, ids, vals, context=None):
738         if context is None:
739             context = {}
740         sync_fields = set(['name', 'description', 'date', 'date_closed', 'date_deadline', 'attendee_ids', 'location', 'class'])
741         if (set(vals.keys()) & sync_fields) and 'oe_update_date' not in vals.keys() and 'NewMeeting' not in context:
742             vals['oe_update_date'] = datetime.now()
743
744         return super(calendar_event, self).write(cr, uid, ids, vals, context=context)
745
746     def copy(self, cr, uid, id, default=None, context=None):
747         default = default or {}
748         default['attendee_ids'] = False
749         if default.get('write_type', False):
750             del default['write_type']
751         elif default.get('recurrent_id', False):
752             default['oe_update_date'] = datetime.now()
753         else:
754             default['oe_update_date'] = False
755         return super(calendar_event, self).copy(cr, uid, id, default, context)
756
757     _columns = {
758         'oe_update_date': fields.datetime('OpenERP Update Date'),
759     }
760
761
762 class calendar_attendee(osv.Model):
763     _inherit = 'calendar.attendee'
764
765     _columns = {
766         'google_internal_event_id': fields.char('Google Calendar Event Id', size=256),
767         'oe_synchro_date': fields.datetime('OpenERP Synchro Date'),
768     }
769     _sql_constraints = [('google_id_uniq', 'unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!')]
770
771     def write(self, cr, uid, ids, vals, context=None):
772         if context is None:
773             context = {}
774
775         for id in ids:
776             ref = vals.get('event_id', self.browse(cr, uid, id, context=context).event_id.id)
777
778             # If attendees are updated, we need to specify that next synchro need an action
779             # Except if it come from an update_from_google
780             if not context.get('curr_attendee', False) and not context.get('NewMeeting', False):
781                 self.pool['calendar.event'].write(cr, uid, ref, {'oe_update_date': datetime.now()}, context)
782         return super(calendar_attendee, self).write(cr, uid, ids, vals, context=context)