From c4dfc941e69faa6394e0ef65515826de4764f6ba Mon Sep 17 00:00:00 2001 From: jke-openerp Date: Fri, 13 Dec 2013 17:27:52 +0100 Subject: [PATCH] [IMP] Move google Id from event to attendee to allow to an event to be on several google calendars bzr revid: jke@openerp.com-20131213162752-q6z3v7yplj3m8p7b --- .../google_base_account.py | 23 +- addons/google_calendar/google_calendar.py | 219 ++++++++++-------- addons/google_calendar/google_calendar.pyc | Bin 23611 -> 24648 bytes 3 files changed, 133 insertions(+), 109 deletions(-) diff --git a/addons/google_base_account/google_base_account.py b/addons/google_base_account/google_base_account.py index 8351ddd7928..4d20a15984c 100644 --- a/addons/google_base_account/google_base_account.py +++ b/addons/google_base_account/google_base_account.py @@ -145,15 +145,15 @@ class google_service(osv.osv_memory): def _do_request(self,cr,uid,uri,params={},headers={},type='POST', context=None): res = False - print "#########################################" - print "### URI : %s ###" % (uri) - print "### HEADERS : %s ###" % (headers) - print "### METHOD : %s ###" % (type) - if type=='GET': - print "### PARAMS : %s ###" % urllib.urlencode(params) - else: - print "### PARAMS : %s ###" % (params) - print "#########################################" +# print "#########################################" +# print "### URI : %s ###" % (uri) +# print "### HEADERS : %s ###" % (headers) +# print "### METHOD : %s ###" % (type) +# if type=='GET': +# print "### PARAMS : %s ###" % urllib.urlencode(params) +# else: +# print "### PARAMS : %s ###" % (params) +# print "#########################################" try: if type.upper() == 'GET' or type.upper() == 'DELETE': @@ -174,10 +174,7 @@ class google_service(osv.osv_memory): else: content=request.read() res = simplejson.loads(content) - print "RESPONSE" - print "==========" - print res - print "==========" + print "=" except urllib2.HTTPError,e: print "ERROR CATCHED : ",e.read() raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong with your request to google"), context=context) diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py index ba862c163e7..fcd7d6af597 100644 --- a/addons/google_calendar/google_calendar.py +++ b/addons/google_calendar/google_calendar.py @@ -248,30 +248,33 @@ class google_calendar(osv.osv): 'location':single_event_dict.get('location',False), 'class':single_event_dict.get('visibility','public'), 'oe_update_date':update_date, - 'google_internal_event_id': single_event_dict.get('id',False), +# 'google_internal_event_id': single_event_dict.get('id',False), }) if single_event_dict.get("recurrence",False): rrule = [rule for rule in single_event_dict["recurrence"] if rule.startswith("RRULE:")][0][6:] result['rrule']=rrule + if type == "write": - return crm_meeting.write(cr, uid, event['id'], result, context=context) + res = crm_meeting.write(cr, uid, event['id'], result, context=context) elif type == "copy": result['recurrence'] = True - return crm_meeting.write(cr, uid, [event['id']], result, context=context) - elif type == "create": - return crm_meeting.create(cr, uid, result, context=context) + res = crm_meeting.write(cr, uid, [event['id']], result, context=context) -################################# -## MANAGE SYNCHRO TO GMAIL ## -################################# + elif type == "create": + res = crm_meeting.create(cr, uid, result, context=context) + + if context['curr_attendee']: + self.pool.get('calendar.attendee').write(cr,uid,[context['curr_attendee']], {'oe_synchro_date':update_date,'google_internal_event_id': single_event_dict.get('id',False)},context) + + return res + def synchronize_events(self, cr, uid, ids, context=None): gc_obj = self.pool.get('google.calendar') self.create_new_events(cr, uid, context=context) - cr.commit() self.bind_recurring_events_to_google(cr, uid, context) cr.commit() @@ -287,21 +290,22 @@ class google_calendar(osv.osv): gc_pool = self.pool.get('google.calendar') crm_meeting = self.pool['crm.meeting'] + att_obj = self.pool['calendar.attendee'] user_obj = self.pool['res.users'] myPartnerID = user_obj.browse(cr,uid,uid,context=context).partner_id.id context_norecurrent = context.copy() context_norecurrent['virtual_id'] = False - new_events_ids = crm_meeting.search(cr, uid,[('partner_ids', 'in', myPartnerID),('google_internal_event_id', '=', False),'|',('recurrent_id', '=', 0),('recurrent_id', '=', False)], context=context_norecurrent) - - for event in crm_meeting.browse(cr, uid, list(set(new_events_ids)), context): - #TODO replace it by a batch - response = self.create_an_event(cr,uid,event,context=context) - update_date = datetime.strptime(response['updated'],"%Y-%m-%dT%H:%M:%S.%fz") - crm_meeting.write(cr, uid, event.id, {'google_internal_event_id': response['id'], 'oe_update_date':update_date}) - #Check that response OK and return according to that - + my_att_ids = att_obj.search(cr, uid,[('partner_id', '=', myPartnerID),('google_internal_event_id', '=', False)], context=context_norecurrent) + for att in att_obj.browse(cr,uid,my_att_ids,context=context): + if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0: + response = self.create_an_event(cr,uid,att.event_id,context=context) + update_date = datetime.strptime(response['updated'],"%Y-%m-%dT%H:%M:%S.%fz") + crm_meeting.write(cr, uid, att.event_id.id, {'oe_update_date':update_date}) + att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_syncro_date':update_date}) + #Check that response OK and return according to that + cr.commit() return True @@ -315,6 +319,8 @@ class google_calendar(osv.osv): 'OE_isInstance':False, 'OE_update':False, 'OE_status':False, + 'OE_attendee_id': False, + 'OE_synchro':False, #GOOGLE 'GG_event' : False, @@ -346,9 +352,10 @@ class google_calendar(osv.osv): } - def update_events(self, cr, uid, context): + def update_events(self, cr, uid, context): crm_meeting = self.pool['crm.meeting'] user_obj = self.pool['res.users'] + att_obj = self.pool['calendar.attendee'] myPartnerID = user_obj.browse(cr,uid,uid,context=context).partner_id.id context_novirtual = context.copy() @@ -359,25 +366,29 @@ class google_calendar(osv.osv): all_new_event_from_google = all_event_from_google.copy() # Select all events from OpenERP which have been already synchronized in gmail - events_ids = crm_meeting.search(cr, uid,[('partner_ids', 'in', myPartnerID),('google_internal_event_id', '!=', False),('oe_update_date','!=', False)],order='google_internal_event_id',context=context_novirtual) - + #events_ids = crm_meeting.search(cr, uid,[('partner_ids', 'in', myPartnerID),('google_internal_event_id', '!=', False),('oe_update_date','!=', False)],order='google_internal_event_id',context=context_novirtual) + my_att_ids = att_obj.search(cr, uid,[('partner_id', '=', myPartnerID),('google_internal_event_id', '!=', False)], context=context_novirtual) event_to_synchronize = {} - for event in crm_meeting.browse(cr, uid, events_ids, context): - base_event_id = event.google_internal_event_id.split('_')[0] + for att in att_obj.browse(cr,uid,my_att_ids,context=context): + event = att.event_id + + base_event_id = att.google_internal_event_id.split('_')[0] if base_event_id not in event_to_synchronize: event_to_synchronize[base_event_id] = {} - if event.google_internal_event_id not in event_to_synchronize[base_event_id]: - event_to_synchronize[base_event_id][event.google_internal_event_id] = self.get_empty_synchro_summarize() + if att.google_internal_event_id not in event_to_synchronize[base_event_id]: + event_to_synchronize[base_event_id][att.google_internal_event_id] = self.get_empty_synchro_summarize() - event_to_synchronize[base_event_id][event.google_internal_event_id]['OE_event'] = event - event_to_synchronize[base_event_id][event.google_internal_event_id]['OE_found'] = True - event_to_synchronize[base_event_id][event.google_internal_event_id]['OE_event_id'] = event.id - event_to_synchronize[base_event_id][event.google_internal_event_id]['OE_isRecurrence'] = event.recurrency - event_to_synchronize[base_event_id][event.google_internal_event_id]['OE_isInstance'] = bool(event.recurrent_id and event.recurrent_id > 0) - event_to_synchronize[base_event_id][event.google_internal_event_id]['OE_update'] = event.oe_update_date - event_to_synchronize[base_event_id][event.google_internal_event_id]['OE_status'] = event.active + event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_attendee_id'] = att.id + event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_event'] = event + event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_found'] = True + event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_event_id'] = event.id + event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_isRecurrence'] = event.recurrency + event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_isInstance'] = bool(event.recurrent_id and event.recurrent_id > 0) + event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_update'] = event.oe_update_date + event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_status'] = event.active + event_to_synchronize[base_event_id][att.google_internal_event_id]['OE_synchro'] = att.oe_synchro_date for event in all_event_from_google.values(): @@ -399,20 +410,10 @@ class google_calendar(osv.osv): event_to_synchronize[base_event_id][event_id]['GG_update'] =event_to_synchronize[base_event_id][event_id]['GG_update'].replace('T',' ').replace('Z','') event_to_synchronize[base_event_id][event_id]['GG_status'] = (event.get('status') != 'cancelled') - - -# print " $ Event IN Google " -# print " $-----------------" -# for ev in all_event_from_google: -# print ' $ %s (%s) [%s]' % (all_event_from_google[ev].get('id'), all_event_from_google[ev].get('sequence'),all_event_from_google[ev].get('status')) -# print " $-----------------" -# print "" -# print " $ Event IN OPENERP " -# print " $------------------" -# for event in crm_meeting.browse(cr, uid, events_ids, context): -# print ' $ %s (%s) [%s]' % (event.google_internal_event_id, event.id,event.active) -# print " $------------------" -# + + ###################### + # PRE-PROCESSING # + ###################### for base_event in event_to_synchronize: for current_event in event_to_synchronize[base_event]: @@ -435,33 +436,40 @@ class google_calendar(osv.osv): event['td_source'] = 'GG' elif event['OE_update'] > event['GG_update']: event['td_source'] = 'OE' - else: - event['td_action'] = "None" - - if event['%s_isRecurrence' % event['td_source']]: - if event['%s_status' % event['td_source']]: - event['td_action'] = "UPDATE" - event['td_comment'] = 'Only need to update, because i\'m active' + + + if event['td_action'] != "None": + if event['%s_isRecurrence' % event['td_source']]: + if event['%s_status' % event['td_source']]: + event['td_action'] = "UPDATE" + event['td_comment'] = 'Only need to update, because i\'m active' + else: + event['td_action'] = "EXCLUDE" + event['td_comment'] = 'Need to Exclude (Me = First event from recurrence) from recurrence' + + elif event['%s_isInstance' % event['td_source']]: + event['td_action'] = "UPDATE" + event['td_comment'] = 'Only need to update, because already an exclu' else: - event['td_action'] = "EXCLUDE" - event['td_comment'] = 'Need to Exclude (Me = First event from recurrence) from recurrence' - - elif event['%s_isInstance' % event['td_source']]: - event['td_action'] = "UPDATE" - event['td_comment'] = 'Only need to update, because already a exclu' - else: - event['td_action'] = "UPDATE" - event['td_comment'] = 'Simply Update... I\'m a single event' + event['td_action'] = "UPDATE" + event['td_comment'] = 'Simply Update... I\'m a single event' else: - event['td_action'] = "None" - event['td_comment'] = 'Not update needed' + if event['OE_synchro'] < event['OE_update']: + event['td_source'] = 'OE' + event['td_action'] = "UPDATE" + event['td_comment'] = 'Event already updated by another user, but not synchro with my google calendar' + + else: + event['td_action'] = "None" + event['td_comment'] = 'Not update needed' else: event['td_action'] = "None" event['td_comment'] = "Both are already deleted" # New in openERP... Create on create_events of synchronize function elif event['OE_found'] and not event['GG_found']: #Has been deleted form gmail + event['td_source'] = 'OE' event['td_action'] = 'DELETE' event['td_comment'] = 'Removed from GOOGLE ?' elif event['GG_found'] and not event['OE_found']: @@ -483,16 +491,22 @@ class google_calendar(osv.osv): event['td_action'] = "CREATE" event['td_comment'] = 'New EVENT CREATE from GMAIL' -# print " $ Event Merged " -# print " $-----------------" + + + ###################### + # DO ACTION # + ###################### for base_event in event_to_synchronize: - print "Base Event : %s " % base_event + #print "Base Event : %s " % base_event event_to_synchronize[base_event] = sorted(event_to_synchronize[base_event].iteritems(),key=operator.itemgetter(0)) for current_event in event_to_synchronize[base_event]: cr.commit() event = current_event[1] + + + ############# - ### DEBUG ### + ### DEBUG ### ############# # print " Real Event %s (%s)" % (current_event[0],event['OE_event_id']) # print " Found OE:%5s vs GG: %5s" % (event['OE_found'],event['GG_found']) @@ -505,6 +519,9 @@ class google_calendar(osv.osv): # print " Source %s" % (event['td_source']) # print " comment %s" % (event['td_comment']) + + context['curr_attendee'] = event.get('OE_attendee_id',False) + actToDo = event['td_action'] actSrc = event['td_source'] if not actToDo: @@ -516,6 +533,10 @@ class google_calendar(osv.osv): if actSrc == 'GG': res = self.update_from_google(cr, uid, False, event['GG_event'], "create", context) event['OE_event_id'] = res + meeting = crm_meeting.browse(cr,uid,res,context=context) + attendee_record_id = att_obj.search(cr, uid, [('partner_id','=', myPartnerID), ('event_id','=',res)], context=context) + self.pool.get('calendar.attendee').write(cr,uid,attendee_record_id, {'oe_synchro_date':meeting.oe_update_date,'google_internal_event_id': event['GG_event']['id']},context) + elif actSrc == 'OE': raise "Should be never here, creation for OE is done before update !" #Add to batch @@ -528,7 +549,6 @@ class google_calendar(osv.osv): if actSrc == 'OE': self.delete_an_event(cr,uid,current_event[0],context=context) elif actSrc == 'GG': - print "NEED TO EXLUDE FROM GMAIL !!!!" new_google_event_id = event['GG_event']['id'].split('_')[1] if 'T' in new_google_event_id: new_google_event_id = new_google_event_id.replace('T','')[:-1] @@ -539,7 +559,6 @@ class google_calendar(osv.osv): parent_event = {} parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].get('OE_event_id') , new_google_event_id) res = self.update_from_google(cr, uid, parent_event, event['GG_event'], "copy", context) - print res else: if event_to_synchronize[base_event][0][1].get('OE_event_id'): parent_oe_id = event_to_synchronize[base_event][0][1].get('OE_event_id') @@ -554,30 +573,36 @@ class google_calendar(osv.osv): def bind_recurring_events_to_google(self, cr, uid, context): crm_meeting = self.pool['crm.meeting'] - + att_obj = self.pool.get('calendar.attendee') user_obj = self.pool['res.users'] myPartnerID = user_obj.browse(cr,uid,uid,context=context).partner_id.id context_norecurrent = context.copy() context_norecurrent['virtual_id'] = False + context_norecurrent['active_test'] = False - new_events_ids = crm_meeting.search(cr, uid,[('partner_ids', 'in', myPartnerID),('google_internal_event_id', '=', False),('recurrent_id', '>', 0),'|',('active', '=', False),('active', '=', True)], context=context_norecurrent) - new_google_internal_event_id = False - - for event in crm_meeting.browse(cr, uid, new_events_ids, context): - source_record = crm_meeting.browse(cr, uid ,event.recurrent_id,context) - - if event.recurrent_id_date and source_record.allday and source_record.google_internal_event_id: - new_google_internal_event_id = source_record.google_internal_event_id +'_'+ event.recurrent_id_date.split(' ')[0].replace('-','') - elif event.recurrent_id_date and source_record.google_internal_event_id: - new_google_internal_event_id = source_record.google_internal_event_id +'_'+ event.recurrent_id_date.replace('-','').replace(' ','T').replace(':','') + 'Z' - - if new_google_internal_event_id: - crm_meeting.write(cr, uid, [event.id], {'google_internal_event_id': new_google_internal_event_id}) + #new_events_ids = crm_meeting.search(cr, uid,[('partner_ids', 'in', myPartnerID),('google_internal_event_id', '=', False),('recurrent_id', '>', 0),'|',('active', '=', False),('active', '=', True)], context=context_norecurrent) + my_att_ids = att_obj.search(cr, uid,[('partner_id', '=', myPartnerID),('google_internal_event_id', '=', False)], context=context_norecurrent) + for att in att_obj.browse(cr,uid,my_att_ids,context=context): + if att.event_id.recurrent_id and att.event_id.recurrent_id > 0: + new_google_internal_event_id = False + source_event_record = crm_meeting.browse(cr, uid, att.event_id.recurrent_id, context) + source_attendee_record_id = att_obj.search(cr, uid, [('partner_id','=', myPartnerID), ('event_id','=',source_event_record.id)], context=context) + source_attendee_record = att_obj.browse(cr, uid, source_attendee_record_id, context) + if source_attendee_record: + source_attendee_record = source_attendee_record[0] + + if att.event_id.recurrent_id_date and source_event_record.allday and source_attendee_record.google_internal_event_id: + new_google_internal_event_id = source_attendee_record.google_internal_event_id +'_'+ att.event_id.recurrent_id_date.split(' ')[0].replace('-','') + elif event.recurrent_id_date and source_attendee_record.google_internal_event_id: + new_google_internal_event_id = source_attendee_record.google_internal_event_id +'_'+ att.event_id.recurrent_id_date.replace('-','').replace(' ','T').replace(':','') + 'Z' - #TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE ! - self.update_recurrent_event_exclu(cr,uid,new_google_internal_event_id,source_record.google_internal_event_id,event,context=context) + if new_google_internal_event_id: + + #TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE ! + res = self.update_recurrent_event_exclu(cr,uid,new_google_internal_event_id,source_attendee_record.google_internal_event_id,att.event_id,context=context) + att_obj.write(cr, uid, [att.event_id.id], {'google_internal_event_id': new_google_internal_event_id}) def check_and_sync(self, cr, uid, oe_event, google_event, context): @@ -609,12 +634,8 @@ class google_calendar(osv.osv): current_user = self.pool.get('res.users').browse(cr,uid,uid,context=context) if datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], "%Y-%m-%d %H:%M:%S") < (datetime.now() + timedelta(minutes=1)): - print "@@ REFRESH TOKEN NEEDED !!!!" self.do_refresh_token(cr,uid,context=context) - print "@@ REFRESH TOKEN DONE !!!!" current_user.refresh() - else: - print "TOKEN OK : ",datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], "%Y-%m-%d %H:%M:%S"), " > ", (datetime.now() - timedelta(minutes=1)) return current_user.google_calendar_token @@ -682,14 +703,11 @@ class crm_meeting(osv.osv): del default['write_type'] elif default.get('recurrent_id', False): default['oe_update_date'] = datetime.now() - default['google_internal_event_id'] = False else: - default['google_internal_event_id'] = False default['oe_update_date'] = False return super(crm_meeting, self).copy(cr, uid, id, default, context) - _columns = { - 'google_internal_event_id': fields.char('Google Calendar Event Id', size=124), + _columns = { 'oe_update_date': fields.datetime('OpenERP Update Date'), } _sql_constraints = [('google_id_uniq','unique(google_internal_event_id)', 'Google ID must be unique!')] @@ -697,12 +715,21 @@ class crm_meeting(osv.osv): class calendar_attendee(osv.osv): _inherit = 'calendar.attendee' + _columns = { + 'google_internal_event_id': fields.char('Google Calendar Event Id', size=124), + 'oe_synchro_date': fields.datetime('OpenERP Synchro Date'), + } def write(self, cr, uid, ids, vals, context=None): + if context is None: + context = {} + for id in ids: ref = vals.get('event_id',self.browse(cr,uid,id,context=context).event_id.id) - #ToDo pass value in context to not force update when attendee come from update_from_google - self.pool.get('crm.meeting').write(cr, uid, ref, {'oe_update_date':datetime.now()},context) + + #No update the date when attendee come from update_from_google + if not context.get('curr_attendee', False): + self.pool.get('crm.meeting').write(cr, uid, ref, {'oe_update_date':datetime.now()},context) return super(calendar_attendee, self).write(cr, uid, ids, vals, context=context) diff --git a/addons/google_calendar/google_calendar.pyc b/addons/google_calendar/google_calendar.pyc index d8cd7cfae0f49b8a8bdf9a55014f83676db49de8..dc0fd0c1df880ddf53eb928753106a2b30341c69 100644 GIT binary patch delta 8171 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 Kwz>lGCI15wt1Ke` delta 7197 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 k4dZ3??UUb>X?6M329G4gvPYCib>!qx1GT>t<8