diff --git a/addons/base_calendar/__init__.py b/addons/base_calendar/__init__.py index 6b05334366e..7a5ab5d78c4 100644 --- a/addons/base_calendar/__init__.py +++ b/addons/base_calendar/__init__.py @@ -21,5 +21,6 @@ import base_calendar import controllers +import res_config # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/base_calendar/__openerp__.py b/addons/base_calendar/__openerp__.py index 04fc2c76d66..613632e30c3 100644 --- a/addons/base_calendar/__openerp__.py +++ b/addons/base_calendar/__openerp__.py @@ -43,7 +43,8 @@ If you need to manage your meetings, you should install the CRM module. 'security/calendar_security.xml', 'security/ir.model.access.csv', 'crm_meeting_view.xml', - 'base_calendar_data.xml' + 'base_calendar_data.xml', + 'res_config_view.xml', ], 'js': [ 'static/src/js/*.js' diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index bcf4b459b36..62d22861e03 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -1509,7 +1509,6 @@ class crm_meeting(osv.Model): self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee(s)"), subtype="base_calendar.subtype_invitation", context=context) return; - def get_attendee(self, cr, uid, meeting_id, context=None): #Used for view in controller invitation = {'meeting':{}, 'attendee': [], 'logo': ''} @@ -1650,13 +1649,11 @@ class crm_meeting(osv.Model): end_date = self._get_recurrency_end_date(data, context=context) super(crm_meeting, self).write(cr, uid, [data['id']], {'end_date': end_date}, context=context) - attendees_create = False - + attendees_create = False if values.get('partner_ids', False): attendees_create = self.create_attendees(cr, uid, ids, context) - - print values - if values.get('date', False): + + if values.get('date', False) and not context.get('install_mode',False): #Not send mail when install data print 'Id notified (ids|new_id) : ',ids,"|",new_id the_id = new_id or (ids and int(ids[0])); @@ -1799,7 +1796,8 @@ class crm_meeting(osv.Model): res = super(crm_meeting, self).unlink(cr, uid, ids, context=context) self.unlink_events(cr, uid, ids, context=context) return res - + + # class mail_mail(osv.osv): # _inherit = "mail.mail" diff --git a/addons/base_calendar/base_calendar_data.xml b/addons/base_calendar/base_calendar_data.xml index 89ca318fc15..f60e61ca2f5 100644 --- a/addons/base_calendar/base_calendar_data.xml +++ b/addons/base_calendar/base_calendar_data.xml @@ -17,35 +17,30 @@ 15 min notif - minutes - notification + notification 30 min notif - minutes notification 1 hour notif - hours notification 2 hours notif - hours notification 1 day notif - days notification @@ -54,35 +49,30 @@ 15 min mail - minutes email 30 min mail - minutes email 1 hour mail - hours email 2 hours mail - hours email 1 day mail - days email @@ -349,10 +339,10 @@
- REMINDER : ${object.name} + ${object.name}
- Hello ${ctx['att_obj'].cn} ,

${object.organizer} invited you for the ${object.name} meeting of ${object.user_id.company_id.name}.

+ Hello ${ctx['att_obj'].cn} ,

${object.user_id.partner_id.name} invited you for the ${object.name} meeting of ${object.user_id.company_id.name}.

@@ -367,7 +357,7 @@
- % if object.location : + % if object.location: % endif - % if not object.location : - - - - - % endif % if object.description : - % endif - % if not object.description : - - - - - % endif + % endif
@@ -383,20 +373,6 @@
-
- Where -
-
-
- : - -
-
@@ -410,21 +386,7 @@
-
- What -
-
-
- : - -
-
@@ -448,6 +410,13 @@
+
+ Accept + Decline +
+
+ --
Sent by ${object.user_id.name} from ${object.user_id.company_id.name}. View this meeting detail directly in OpenERP. +
diff --git a/addons/base_calendar/crm_meeting_demo.xml b/addons/base_calendar/crm_meeting_demo.xml index 9cd53d0fc6c..690e65c1583 100644 --- a/addons/base_calendar/crm_meeting_demo.xml +++ b/addons/base_calendar/crm_meeting_demo.xml @@ -9,6 +9,7 @@ + Follow-up for Project proposal Meeting to discuss project plan and hash out the details of implementation. @@ -20,7 +21,7 @@ - + Initial discussion Discussion with partner for product. @@ -32,7 +33,7 @@ - + Pricing Discussion Internal meeting for discussion for new pricing for product and services. @@ -44,7 +45,7 @@ - + Requirements review @@ -55,7 +56,7 @@ - + Changes in Designing @@ -66,7 +67,7 @@ - + Presentation for new Services diff --git a/addons/base_calendar/crm_meeting_view.xml b/addons/base_calendar/crm_meeting_view.xml index bb70a643f1a..b5408b4198d 100644 --- a/addons/base_calendar/crm_meeting_view.xml +++ b/addons/base_calendar/crm_meeting_view.xml @@ -27,7 +27,7 @@ CRM - Meetings Form crm.meeting - 1 +
@@ -158,7 +158,7 @@ Meetings Popup crm.meeting - 5 + @@ -310,7 +310,7 @@ form,calendar,tree,gantt - + diff --git a/addons/google_base_account/__init__.py b/addons/google_base_account/__init__.py index 44a5aabe277..ee900c8febc 100644 --- a/addons/google_base_account/__init__.py +++ b/addons/google_base_account/__init__.py @@ -18,8 +18,8 @@ # along with this program. If not, see . # ############################################################################## - import google_base_account +import controllers # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/google_base_account/controllers/__init__.py b/addons/google_base_account/controllers/__init__.py new file mode 100644 index 00000000000..8ee9bae18d9 --- /dev/null +++ b/addons/google_base_account/controllers/__init__.py @@ -0,0 +1 @@ +import main diff --git a/addons/google_base_account/controllers/main.py b/addons/google_base_account/controllers/main.py new file mode 100644 index 00000000000..ce7909da6cf --- /dev/null +++ b/addons/google_base_account/controllers/main.py @@ -0,0 +1,119 @@ +import simplejson +import urllib +import openerp +import openerp.addons.web.http as http +from openerp.addons.web.http import request +import openerp.addons.web.controllers.main as webmain +import json +from openerp.addons.web.http import SessionExpiredException +from werkzeug.exceptions import BadRequest +import werkzeug.utils + +class google_auth(http.Controller): + + @http.route('/googleauth/oauth2callback', type='http', auth="none") + def oauth2callback(self, **kw): + + state = simplejson.loads(kw['state']) + + #action = state.get('a') + #menu = state.get('m') + dbname = state.get('d') + #service = state.get('s') + url_return = state.get('from') + + registry = openerp.modules.registry.RegistryManager.get(dbname) + with registry.cursor() as cr: + #TODO CHECK IF REQUEST OK + registry.get('google.calendar').set_all_tokens(cr,request.session.uid,kw['code']) + registry.get('google.calendar').set_primary_id(cr,request.session.uid) + + return werkzeug.utils.redirect(url_return) + + #@openerp.addons.web.http.route('/web_calendar_sync/sync_calendar/sync_data', type='json', auth='user') + @http.route('/web_calendar_sync/sync_calendar/sync_data', type='json', auth='user') + def sync_data(self, arch, fields, model,**kw): + calendar_info = { + 'field_data':{}, + 'date_start':arch['attrs'].get('date_start'), + 'date_stop':arch['attrs'].get('date_stop'), + 'calendar_string':arch['attrs'].get('string'), + 'model':model + } + for field, data in fields.items(): + calendar_info['field_data'][field] = { + 'type': data.get('type'), + 'string': data.get('string') + } + + if model == 'crm.meeting': + model_obj = request.registry.get('crm.meeting.synchronize') + gc_obj = request.registry.get('google.calendar') + + #We check that user has already accepted openerp to acces his calendar ! + if not gc_obj.get_refresh_token(request.cr, request.uid,context=kw.get('LocalContext')): + url = gc_obj.authorize_google_uri(request.cr, request.uid, from_url=kw.get('fromurl'),context=kw.get('LocalContext')) + return { + "status" : "NeedAuth", + "url" : url + } + + #We lunch th synchronization + print "ORI COONTEXT = ",kw.get('LocalContext') + model_obj.synchronize_events(request.cr, request.uid, [], kw.get('LocalContext')) + else: + model_obj = request.registry.get('google.calendar') + model_obj.synchronize_calendar(request.cr, request.uid, calendar_info, kw.get('LocalContext')) + + + return { "status" : "SUCCESS" } + + + + + + + + + + + + + + + + + + + + + + + + + + + @http.route('/googleauth/AuthorizeMe', type='http', auth="none") + def authorize_app(self,**val): + if val.get('done'): + return; + registry = openerp.modules.registry.RegistryManager.get(request.session.get('db')) + gs_pool = registry.get('google.service') + with registry.cursor() as cr: + url = gs_pool._get_authorize_uri(cr,request.session.uid,service='calendar',from_url='') + return werkzeug.utils.redirect(url) ##REDIRECT WHERE THE USER WAS BEFORE (with state) + + + @http.route('/googleauth/GiveMeAToken', type='http', auth="none") + def authorize_me(self,**val): + registry = openerp.modules.registry.RegistryManager.get(request.session.get('db')) + gs_pool = registry.get('google.service') + with registry.cursor() as cr: + token = gs_pool._get_google_token_json(cr, request.session.uid, 'api_code') + + print '#####################################' + print '## YOUR TOKEN : ',token, " ##" + print '#####################################' + #return werkzeug.utils.redirect(url) + return + diff --git a/addons/google_base_account/google_base_account.py b/addons/google_base_account/google_base_account.py index 4f85db0582b..e45a599be51 100644 --- a/addons/google_base_account/google_base_account.py +++ b/addons/google_base_account/google_base_account.py @@ -22,6 +22,7 @@ from openerp.osv import osv from openerp import SUPERUSER_ID from openerp.tools.translate import _ +from openerp.addons.web.http import request import urllib import urllib2 @@ -57,9 +58,135 @@ class google_service(osv.osv_memory): 'redirect_uri': ir_config.get_param(cr, SUPERUSER_ID, 'google_redirect_uri'), 'client_id': ir_config.get_param(cr, SUPERUSER_ID, 'google_%s_client_id' % service), 'response_type': 'code', - 'client_id': ir_config.get_param(cr, SUPERUSER_ID, 'google_%s_client_id' % service), + 'client_id': ir_config.get_param(cr, SUPERUSER_ID, 'google_%s_client_id' % service), } uri = 'https://accounts.google.com/o/oauth2/auth?%s' % urllib.urlencode(params) return uri - -# vim:expandtab:smartindent:toabstop=4:softtabstop=4:shiftwidth=4: + + #If no scope is passed, we use service by default to get a default scope + + def _get_authorize_uri(self, cr, uid, from_url, service, scope = False, context=None): #authorize_API + if not service: + print 'please specify a service (Calendar for example)!' + + state_obj = {} + state_obj['d'] = cr.dbname + state_obj['a'] = request and request.params.get('action') or False + state_obj['m'] = request and request.params.get('menu_id') or False + state_obj['s'] = service + state_obj['from'] = from_url + + base_url = self.get_base_url(cr, uid, context) + client_id = self.get_client_id(cr, uid, service, context) + + params = { + 'response_type': 'code', + 'client_id': client_id, + 'state' : simplejson.dumps(state_obj), + 'scope': scope or 'https://www.googleapis.com/auth/%s' % (service,), + 'redirect_uri': base_url + '/googleauth/oauth2callback', + 'approval_prompt':'force', + 'access_type':'offline' + } + + uri = self.get_uri_oauth(a='auth') + "?%s" % urllib.urlencode(params) + return uri + + def _get_google_token_json(self, cr, uid, authorize_code, service, context=None): #exchange_AUTHORIZATION vs Token (service = calendar) + res = False + base_url = self.get_base_url(cr, uid, context) + client_id = self.get_client_id(cr, uid, service, context) + client_secret = self.get_client_secret(cr, uid, service, context) + + params = { + 'code': authorize_code, + 'client_id': client_id, + 'client_secret': client_secret, + 'grant_type' : 'authorization_code', + 'redirect_uri': base_url + '/googleauth/oauth2callback' + } + + headers = {"content-type": "application/x-www-form-urlencoded"} + + try: + data = urllib.urlencode(params) + req = urllib2.Request(self.get_uri_oauth(a='token'), data, headers) + content = urllib2.urlopen(req).read() + res = simplejson.loads(content) + + except urllib2.HTTPError,e: + print e + raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired"), context=context) + + return res + + def _refresh_google_token_json(self, cr, uid, refresh_token, service, context=None): #exchange_AUTHORIZATION vs Token (service = calendar) + res = False + base_url = self.get_base_url(cr, uid, context) + client_id = self.get_client_id(cr, uid, service, context) + client_secret = self.get_client_secret(cr, uid, service, context) + + params = { + 'refresh_token': refresh_token, + 'client_id': client_id, + 'client_secret': client_secret, + 'grant_type' : 'refresh_token' + } + + headers = {"content-type": "application/x-www-form-urlencoded"} + + try: + data = urllib.urlencode(params) + req = urllib2.Request(self.get_uri_oauth(a='token'), data, headers) + content = urllib2.urlopen(req).read() + res = simplejson.loads(content) + except urllib2.HTTPError: + raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired"), context=context) + + return res + + 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 "#########################################" + + try: + if type.upper() == 'GET': + data = urllib.urlencode(params) + req = urllib2.Request(self.get_uri_api() + uri + "?" + data)#,headers) + elif type.upper() == 'POST': + req = urllib2.Request(self.get_uri_api() + uri, params, headers) + else: + raise ('Method not supported [GET or POST] not in [%s]!' % (type)) + + content = urllib2.urlopen(req).read() + res = simplejson.loads(content) + except urllib2.HTTPError,e: + print e.read() + raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired"), context=context) + + return res + + def get_base_url(self, cr, uid, context=None): + return self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url',default='http://www.openerp.com?NoBaseUrl',context=context) + + def get_client_id(self, cr, uid, service, context=None): + return self.pool.get('ir.config_parameter').get_param(cr, uid, 'google_%s_client_id' % (service,),context=context) + + def get_client_secret(self, cr, uid, service, context=None): + return self.pool.get('ir.config_parameter').get_param(cr, uid, 'google_%s_client_secret' % (service,),context=context) + + def get_uri_oauth(self,a=''): #a = optionnal action + return "https://accounts.google.com/o/oauth2/%s" % (a,) + + def get_uri_api(self): + return 'https://www.googleapis.com' diff --git a/addons/google_calendar/__init__.py b/addons/google_calendar/__init__.py new file mode 100644 index 00000000000..0b6021f1d0d --- /dev/null +++ b/addons/google_calendar/__init__.py @@ -0,0 +1,2 @@ +import google_calendar +import wizard \ No newline at end of file diff --git a/addons/google_calendar/__init__.pyc b/addons/google_calendar/__init__.pyc new file mode 100644 index 00000000000..66f01ec9928 Binary files /dev/null and b/addons/google_calendar/__init__.pyc differ diff --git a/addons/google_calendar/__openerp__.py b/addons/google_calendar/__openerp__.py new file mode 100644 index 00000000000..c11cd989058 --- /dev/null +++ b/addons/google_calendar/__openerp__.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2012 OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +{ + 'name': 'Google Calendar', + 'version': '1.0', + 'category': 'Tools', + 'description': """ +The module adds the possibility to synchronize Google Calendar with OpenERP +======================================== +""", + 'author': 'OpenERP SA', + 'website': 'http://www.openerp.com', + 'depends': ['base_calendar'], + 'js': ['static/src/js/*.js'], + 'qweb': ['static/src/xml/*.xml'], + 'data': [ + 'google_calendar_view.xml', + 'google_calendar_data.xml', + 'wizard/crm_meeting_view.xml', + ], + 'demo': [], + 'installable': True, + 'auto_install': False, +} + diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py new file mode 100644 index 00000000000..c3d343a70db --- /dev/null +++ b/addons/google_calendar/google_calendar.py @@ -0,0 +1,250 @@ +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2012 OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import simplejson +import re +import urllib +import urllib2 + + +from openerp import tools +from openerp.tools.translate import _ + +from openerp.addons.web.http import request +import werkzeug.utils + +from datetime import datetime, timedelta, date + +from openerp.osv import fields, osv +from openerp.osv import osv + + +google_state_mapping = { + 'needs-action':'needsAction', + 'declined': 'declined', + 'tentative':'tentative', + 'accepted':'accepted', + 'delegated':'declined', +} +oe_state_mapping = { + 'needsAction':'needs-action', + 'declined': 'declined', + 'tentative':'tentative', + 'accepted':'accepted', +} + +class google_calendar(osv.osv): + _name = 'google.calendar' + + STR_SERVICE = 'calendar' + + def authorize_google_uri(self,cr,uid,from_url='http://www.openerp.com',context=None): + url = self.pool.get('google.service')._get_authorize_uri(cr,uid,from_url,self.STR_SERVICE,scope=self.get_calendar_scope(),context=context) + return url + + def set_all_tokens(self,cr,uid,authorization_code,context=None): + gs_pool = self.pool.get('google.service') + all_token = gs_pool._get_google_token_json(cr, uid, authorization_code,self.STR_SERVICE,context=context) + + vals = {} + vals['google_%s_rtoken' % self.STR_SERVICE] = all_token.get('refresh_token') + vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in')) #NEED A CALCUL + vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') + self.pool.get('res.users').write(cr,uid,uid,vals,context=context) + + def set_primary_id(self,cr,uid,context=None): + gs_pool = self.pool.get('google.service') + + params = { + 'fields': 'id', + 'access_token': self.get_token(cr, uid, context=context) + } + + cal = gs_pool._do_request(cr, uid, "/calendar/v3/calendars/primary/", params, type='GET', context=context) + + if cal.get('id',False): + vals = {} + vals['google_calendar_id']= cal.get('id') + self.pool.get('res.users').write(cr,uid,uid,vals,context=context) + return True + else: + return False + + def generate_data(self, cr, uid, event, context=None): + if event.allday: + start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=0), context=context).isoformat('T').split('T')[0] + end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=24), context=context).isoformat('T').split('T')[0] + type = 'date' + else: + start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') + end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') + type = 'dateTime' + attendee_list = [] + + for attendee in event.attendee_ids: + attendee_list.append({ + 'email':attendee.email or 'NoEmail@mail.com', + 'displayName':attendee.partner_id.name, + 'responseStatus':google_state_mapping.get(attendee.state, 'needsAction'), + }) + data = { + "summary": event.name or '', + "description": event.description or '', + "start":{ + type:start_date, + #'timeZone':context.get('tz') or 'UTC' + }, + "end":{ + type:end_date, + #'timeZone':context.get('tz') or 'UTC' + }, + "attendees":attendee_list, + "location":event.location or '', + "visibility":event['class'] or 'public', + } + if event.recurrency and event.rrule: + data["recurrence"]= [] + if event.exdate: + data["recurrence"].append("EXDATE:"+event.exdate) + + data["recurrence"]+=["RRULE:"+event.rrule] + + + #if not recurrency and event.recurrent_id and event.recurrent_id != 0: ###" IMMUTABLE + # data["recurringEventId"] = event.recurrent_id + + print data + return data + + def create_event(self, cr, uid,event, context=None): + gs_pool = self.pool.get('google.service') + + print "CONTEXT : ",context + data = self.generate_data(cr, uid,event, context=context) + + + url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary',urllib.quote('id,updated'),self.get_token(cr,uid,context)) + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + data_json = simplejson.dumps(data) + + response = gs_pool._do_request(cr, uid, url, data_json, headers, type='POST', context=context) + #TODO Check response result + + return response + + def get_token(self,cr,uid,context=None): + 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=5): + print "NEED TO RENEW TOKEN",current_user.google_calendar_token_validity , "<", datetime.now() - timedelta(minutes=5) + self.do_refresh_token(cr,uid,context=context) + current_user.refresh() + else: + print "KEEP OLD TOKEN",current_user.google_calendar_token_validity , "<", datetime.now() - timedelta(minutes=5) + return current_user.google_calendar_token + + def do_refresh_token(self,cr,uid,context=None): + current_user = self.pool.get('res.users').browse(cr,uid,uid,context=context) + gs_pool = self.pool.get('google.service') + + refresh = current_user.google_calendar_rtoken + all_token = gs_pool._refresh_google_token_json(cr, uid, current_user.google_calendar_rtoken,self.STR_SERVICE,context=context) + + vals = {} + vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in')) + vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') + + self.pool.get('res.users').write(cr,uid,uid,vals,context=context) + + + def get_refresh_token(self,cr,uid,context=None): + current_user = self.pool.get('res.users').browse(cr,uid,uid,context=context) + return current_user.google_calendar_rtoken + + def get_calendar_scope(self,RO=False): + readonly = RO and '.readonly' or '' + return 'https://www.googleapis.com/auth/calendar%s' % (readonly) + +class res_users(osv.osv): + _inherit = 'res.users' + + _columns = { + 'google_calendar_id': fields.char('Primary Calendar ID'), + 'google_calendar_rtoken': fields.char('Refresh Token'), + 'google_calendar_token': fields.char('User token'), + 'google_calendar_token_validity': fields.datetime('Token Validity'), + } + + def get_cal_token_info(self, cr, uid, partner_id=False, context=None): + if partner_id: + user = self.pool.get('res.partner').browse(cr,uid,partner_id,context=context).user_id + else: + user = self.pool.get('res.users').browse(cr,uid,uid,context=context) + + return dict(authcode=user.google_cal_authcode, token=user.google_cal_token, validity=user.google_cal_token_validity,) + + def update_cal_token(self,cr,uid,jsontoken,context=None): + print jsontoken + import ipdb; ipdb.set_trace(); + self.write(cr,uid,uid,{'xxx' : datetime.now() } ,context=context) + return "OK" + + + +class crm_meeting(osv.osv): + _inherit = "crm.meeting" + + def write(self, cr, uid, ids, vals, context=None): + sync_fields = set(['name', 'description', 'date', 'date_closed', 'date_deadline', 'attendee_ids', 'location', 'class']) + if (set(vals.keys()) & sync_fields) and 'oe_update_date' not in vals.keys(): + vals['oe_update_date'] = datetime.now() + return super(crm_meeting, self).write(cr, uid, ids, vals, context=context) + + def copy(self, cr, uid, id, default=None, context=None): + default = default or {} + default['attendee_ids'] = False + if default.get('write_type', False): + 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), + 'oe_update_date': fields.datetime('OpenERP Update Date'), + } + +# If attendees are updated, we need to specify that next synchro need an action +class calendar_attendee(osv.osv): + _inherit = 'calendar.attendee' + + def write(self, cr, uid, ids, vals, context=None): + if isinstance(ids, list): + cr.execute("SELECT crmmeeting_id FROM crmmeeting_attendee_rel WHERE attendee_id = %s"%(ids[0])) + else: + cr.execute("SELECT crmmeeting_id FROM crmmeeting_attendee_rel WHERE attendee_id = %s"%(ids)) + event_id = cr.fetchone()[0] + if event_id: + self.pool.get('crm.meeting').write(cr, uid, event_id, {'oe_update_date':datetime.now()}) + 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 new file mode 100644 index 00000000000..7f2a58b5ba7 Binary files /dev/null and b/addons/google_calendar/google_calendar.pyc differ diff --git a/addons/google_calendar/google_calendar_data.xml b/addons/google_calendar/google_calendar_data.xml new file mode 100644 index 00000000000..dadfaa915e3 --- /dev/null +++ b/addons/google_calendar/google_calendar_data.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/addons/google_calendar/google_calendar_view.xml b/addons/google_calendar/google_calendar_view.xml new file mode 100644 index 00000000000..e99826dae52 --- /dev/null +++ b/addons/google_calendar/google_calendar_view.xml @@ -0,0 +1,65 @@ + + + + + + + + ir.attachment.google.spreadsheet.tree + ir.attachment + + + + + + + + + + ir.attachment.google.spreadsheet.form + ir.attachment + + + + + + + + + + + + + + + + + + Google Spreadsheets + ir.attachment + form + tree,form + {} + [('url', 'like', '/spreadsheet/')] + Google Spreadsheets + + + + + tree + + + + + + + form + + + + + + + \ No newline at end of file diff --git a/addons/google_calendar/static/src/img/calendar-16.png b/addons/google_calendar/static/src/img/calendar-16.png new file mode 100644 index 00000000000..520c37f5f7b Binary files /dev/null and b/addons/google_calendar/static/src/img/calendar-16.png differ diff --git a/addons/google_calendar/static/src/img/calendar_32.png b/addons/google_calendar/static/src/img/calendar_32.png new file mode 100644 index 00000000000..40dd085f4eb Binary files /dev/null and b/addons/google_calendar/static/src/img/calendar_32.png differ diff --git a/addons/google_calendar/static/src/js/calendar_sync.js b/addons/google_calendar/static/src/js/calendar_sync.js new file mode 100644 index 00000000000..62639c6771f --- /dev/null +++ b/addons/google_calendar/static/src/js/calendar_sync.js @@ -0,0 +1,40 @@ +/*--------------------------------------------------------- + * OpenERP web_calendar + *---------------------------------------------------------*/ + +openerp.google_calendar = function(instance) { + var _t = instance.web._t, + _lt = instance.web._lt; + var QWeb = instance.web.qweb; + + instance.web_calendar.FullCalendarView.include({ + view_loading: function(r) { + var self = this; + this.$el.on('click', 'div.oe_dhx_cal_sync_button', function() { + self.sync_calendar(r); + }); + return this._super(r); + }, + sync_calendar: function(res) { + var self = this; + var context = instance.web.pyeval.eval('context'); + console.log("Model is..." + res.model); + + self.rpc('/web_calendar_sync/sync_calendar/sync_data', { + arch: res.arch, + fields: res.fields, + model:res.model, + fromurl: window.location.href, + LocalContext:context + }).always(function(o) { + if (o.status == "NeedAuth") { + alert(_t("You will be redirected on gmail to authorize your OpenErp to access your calendar !")); + window.location = o.url; + } + console.log(o); + //self.reload(); + }); + } + }); + +}; diff --git a/addons/google_calendar/static/src/xml/web_calendar.xml b/addons/google_calendar/static/src/xml/web_calendar.xml new file mode 100644 index 00000000000..7baadaaf183 --- /dev/null +++ b/addons/google_calendar/static/src/xml/web_calendar.xml @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/addons/google_calendar/wizard/__init__.py b/addons/google_calendar/wizard/__init__.py new file mode 100644 index 00000000000..7fbf7fc2a2c --- /dev/null +++ b/addons/google_calendar/wizard/__init__.py @@ -0,0 +1 @@ +import crm_meeting \ No newline at end of file diff --git a/addons/google_calendar/wizard/crm_meeting.py b/addons/google_calendar/wizard/crm_meeting.py new file mode 100644 index 00000000000..88484036134 --- /dev/null +++ b/addons/google_calendar/wizard/crm_meeting.py @@ -0,0 +1,269 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import osv, fields +from openerp import SUPERUSER_ID +from openerp import tools +from openerp.tools.translate import _ +from datetime import datetime +from dateutil import parser + + +import pytz +import urllib +import urllib2 +import json +import werkzeug.utils + +class crm_meeting_synchronize(osv.osv_memory): + _name = 'crm.meeting.synchronize' + + + def synchronize_events(self, cr, uid, ids, context=None): + gc_obj = self.pool.get('google.calendar') + + self.create_new_events(cr, uid, context=context) + #self.bind_recurring_events_to_google(cr, uid, context) + #self.update_events(cr, uid, access_token, context) + return True +# +# def generate_data(self, cr, uid, event, context): +# if event.allday: +# start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T').split('T')[0] +# end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T').split('T')[0] +# type = 'date' +# else: +# start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') +# end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T') +# type = 'dateTime' +# attendee_list = [] +# for attendee in event.attendee_ids: +# attendee_list.append({ +# 'email':attendee.email, +# 'displayName':attendee.partner_id.name, +# 'responseStatus':google_state_mapping.get(attendee.state, 'needsAction'), +# }) +# data = { +# "summary": event.name or '', +# "description": event.description or '', +# "start":{ +# type:start_date, +# 'timeZone':context.get('tz') +# }, +# "end":{ +# type:end_date, +# 'timeZone':context.get('tz') +# }, +# "attendees":attendee_list, +# "colorId":4, +# "location":event.location or '', +# "visibility":event['class'] or 'public', +# } +# if event.rrule: +# data["recurrence"]=["RRULE:"+event.rrule] +# return data +# + def create_new_events(self, cr, uid, context): + gc_pool = self.pool.get('google.calendar') + + crm_meeting = self.pool['crm.meeting'] + user_obj = self.pool['res.users'] + + print "TO CHECK RESULT HERE >>>>>>>>>>>>>>>>>>>>" + + + + + context_norecurrent = context.copy() + context_norecurrent['virtual_id'] = False + + new_events_ids = crm_meeting.search(cr, uid,[('partner_ids', 'in', user_obj.browse(cr,uid,uid,context=context).partner_id.id),('google_internal_event_id', '=', False)], context=context_norecurrent) + #new_events_ids = [str(i).split('-')[0] for i in new_events_ids] + #new_events_ids = base_calendar_id2real_id(new_events_ids,False); #[str(i).split('-')[0] for i in new_events_ids] + + print 'Events ids [new_events_ids] : ', new_events_ids + + for event in crm_meeting.browse(cr, uid, list(set(new_events_ids)), context): + response = gc_pool.create_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 + + return True +# +# def bind_recurring_events_to_google(self, cr, uid, context): +# crm_meeting = self.pool['crm.meeting'] +# new_events_ids = crm_meeting.search(cr, uid,[('user_id', '=', uid),('google_internal_event_id', '=', False),('recurrent_id', '>', 0)], context) +# new_events_ids = base_calendar_id2real_id(new_events_ids,False); #[str(i).split('-')[0] for i in new_events_ids] +# +# for event in crm_meeting.browse(cr, uid, list(set(new_events_ids)), context): +# source_record = crm_meeting.read(cr, uid ,event.recurrent_id,['allday', 'google_internal_event_id'],context) +# new_google_internal_event_id = False +# if event.recurrent_id_date and source_record.get('allday') and source_record.get('google_internal_event_id'): +# new_google_internal_event_id = source_record.get('google_internal_event_id') +'_'+ event.recurrent_id_date.split(' ')[0].replace('-','') + 'Z' +# elif event.recurrent_id_date and source_record.get('google_internal_event_id'): +# new_google_internal_event_id = source_record.get('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}) + + def get_event_dict(self,access_token, nextPageToken): + request_url = "https://www.googleapis.com/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary',urllib.quote('items,nextPageToken') ,access_token) + if nextPageToken: + request_url += "&pageToken=%s" %(nextPageToken) + try: + req = urllib2.Request(request_url) + content = urllib2.urlopen(req).read() + except urllib2.HTTPError,e: + error_message = e.read() + print error_message + content = json.loads(content) + google_events_dict = {} + for google_event in content['items']: + if google_event.get('updated',False): + google_events_dict[google_event['id']] = google_event + if content.get('nextPageToken', False): + google_events_dict.update(self.get_event_dict(access_token,content['nextPageToken'])) + return google_events_dict + + def update_events(self, cr, uid, access_token, context): + crm_meeting = self.pool['crm.meeting'] + google_event_dict = self.get_event_dict(access_token, False) + updated_events_ids = crm_meeting.search(cr, uid,[('partner_ids', 'in', user_obj.browse(cr,uid,uid,context=context)),('google_internal_event_id', '!=', False),('oe_update_date','!=', False)], context) + for event in crm_meeting.browse(cr, uid, list(set(updated_events_ids)), context): + if event.google_internal_event_id in google_event_dict: + self.check_and_sync(cr, uid, access_token, event, google_event_dict[event.google_internal_event_id], context) + del google_event_dict[event.google_internal_event_id] + else: + request_url = "https://www.googleapis.com/calendar/v3/calendars/%s/events/%s?access_token=%s" % ('primary', event.google_internal_event_id, access_token) + content = {} + try: + req = urllib2.Request(request_url) + content = urllib2.urlopen(req).read() + except urllib2.HTTPError,e: + error_message = e.read() + print error_message + if e.code == 404: + print "Need DELETE" + if content: + content = json.loads(content) + self.check_and_sync(cr, uid, access_token, event, content, context) + for new_google_event in google_event_dict.values(): + if new_google_event.get('recurringEventId',False): + reccurent_event = crm_meeting.search(cr, uid, [('google_internal_event_id', '=', new_google_event['recurringEventId'])]) + new_google_event_id = new_google_event['id'].split('_')[1].replace('T','')[:-1] + for event_id in reccurent_event: + if isinstance(event_id, str) and len(event_id.split('-'))>1 and event_id.split('-')[1] == new_google_event_id: + reccurnt_event_id = int(event_id.split('-')[0].strip()) + parent_event = crm_meeting.read(cr,uid, reccurnt_event_id, [], context) + parent_event['id'] = event_id + #reccurent update from google + self.update_from_google(cr, uid, parent_event, new_google_event, "copy", context) + else: + #new event from google + self.update_from_google(cr, uid, False, new_google_event, "create", context) +# del google_events_dict[new_google_event['id']] + + def check_and_sync(self, cr, uid, access_token, oe_event, google_event, context): + if datetime.strptime(oe_event.oe_update_date,"%Y-%m-%d %H:%M:%S.%f") > datetime.strptime(google_event['updated'],"%Y-%m-%dT%H:%M:%S.%fz"): + #update to google + self.update_to_google(cr, uid, access_token, oe_event, google_event, context) + elif datetime.strptime(oe_event.oe_update_date,"%Y-%m-%d %H:%M:%S.%f") < datetime.strptime(google_event['updated'],"%Y-%m-%dT%H:%M:%S.%fz"): + #update from google + self.update_from_google(cr, uid, oe_event, google_event, 'write', context) + pass + + def update_to_google(self, cr, uid, access_token, oe_event, google_event, context): + crm_meeting = self.pool['crm.meeting'] + request_url = "https://www.googleapis.com/calendar/v3/calendars/%s/events/%s?fields=%s" % ('primary', google_event['id'], urllib.quote('id,updated')) + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + data = self.generate_data(cr,uid ,oe_event, context) + data['sequence'] = google_event.get('sequence', 0) + data_json = json.dumps(data) + try: + req = urllib2.Request(request_url, data_json, headers) + req.get_method = lambda: 'PATCH' + content = urllib2.urlopen(req).read() + except urllib2.HTTPError,e: + error_message = json.loads(e.read()) + + content = json.loads(content) + update_date = datetime.strptime(content['updated'],"%Y-%m-%dT%H:%M:%S.%fz") + crm_meeting.write(cr, uid, [oe_event.id], {'oe_update_date':update_date}) + + def update_from_google(self, cr, uid, event, single_event_dict, type, context): + crm_meeting = self.pool['crm.meeting'] + res_partner_obj = self.pool['res.partner'] + calendar_attendee_obj = self.pool['calendar.attendee'] + attendee_record= [] + result = {} + if single_event_dict.get('attendees',False): + for google_attendee in single_event_dict['attendees']: + if type == "write": + for oe_attendee in event['attendee_ids']: + if calendar_attendee_obj.browse(cr, uid ,oe_attendee,context=context).email == google_attendee['email']: + calendar_attendee_obj.write(cr, uid,[oe_attendee] ,{'state' : oe_state_mapping[google_attendee['responseStatus']]}) + google_attendee['found'] = True + if google_attendee.get('found',False): + continue + attendee_id = res_partner_obj.search(cr, uid,[('email', '=', google_attendee['email'])], context=context) + if not attendee_id: + attendee_id = [res_partner_obj.create(cr, uid,{'email': google_attendee['email'], 'name': google_attendee.get("displayName",False) or google_attendee['email'] }, context=context)] + attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context) + attendee['partner_id'] = attendee.pop('id') + attendee['state'] = oe_state_mapping[google_attendee['responseStatus']] + attendee_record.append((0, 0, attendee)) + if single_event_dict['start'].get('dateTime',False) and single_event_dict['end'].get('dateTime',False): + UTC = pytz.timezone('UTC') + date = parser.parse(single_event_dict['start']['dateTime']) + date_deadline = parser.parse(single_event_dict['end']['dateTime']) + delta = date_deadline.astimezone(UTC) - date.astimezone(UTC) + result['duration'] = (delta.seconds / 60) / 60.0 + delta.days *24 + date = str(date.astimezone(UTC))[:-6] + date_deadline = str(date_deadline.astimezone(UTC))[:-6] + allday = False + else: + date = single_event_dict['start']['date'] + ' 12:00:00' + date_deadline = single_event_dict['start']['date'] + ' 12:00:00' + allday = True + update_date = datetime.strptime(single_event_dict['updated'],"%Y-%m-%dT%H:%M:%S.%fz") + result.update({ + 'attendee_ids': attendee_record, + 'date': date, + 'date_deadline': date_deadline, + 'allday': allday, + 'name': single_event_dict.get('summary','Event'), + 'description': single_event_dict.get('description',''), + 'location':single_event_dict.get('location',''), + 'class':single_event_dict.get('visibility','public'), + 'oe_update_date':update_date, + 'google_internal_event_id': single_event_dict.get('id',''), + }) + 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": + crm_meeting.write(cr, uid, event['id'], result, context=context) + elif type == "copy": + result['write_type'] = "copy" + crm_meeting.write(cr, uid, event['id'], result, context=context) + elif type == "create": + crm_meeting.create(cr, uid, result, context=context) \ No newline at end of file diff --git a/addons/google_calendar/wizard/crm_meeting_view.xml b/addons/google_calendar/wizard/crm_meeting_view.xml new file mode 100644 index 00000000000..88a1c34b481 --- /dev/null +++ b/addons/google_calendar/wizard/crm_meeting_view.xml @@ -0,0 +1,35 @@ + + + + + Synchronize Events to Google + crm.meeting.synchronize + +
+ + +
+
+
+
+
+ + + Synchronize Events to Google + ir.actions.act_window + crm.meeting.synchronize + form + form + + {} + new + + + +
+
\ No newline at end of file