res_users_settings.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from datetime import timedelta
  4. import logging
  5. from odoo import api, fields, models, _
  6. from odoo.exceptions import UserError
  7. _logger = logging.getLogger(__name__)
  8. class ResUsersSettings(models.Model):
  9. _inherit = "res.users.settings"
  10. # Google API tokens and synchronization information
  11. google_rtoken = fields.Char('Google Refresh Token', copy=False, groups='base.group_system')
  12. google_token = fields.Char('Google User Token', copy=False, groups='base.group_system')
  13. google_token_validity = fields.Datetime('Google Token Validity', copy=False, groups='base.group_system')
  14. google_email = fields.Char('Google Email', copy=False, groups='base.group_system')
  15. google_sync_enabled = fields.Boolean('Google Sync Enabled', default=False, copy=False, groups='base.group_system')
  16. # CRM Meets Files Configuration
  17. google_crm_meets_folder_id = fields.Char(
  18. string='Archivos Meets CRM',
  19. help='ID de la carpeta en Google Drive donde se almacenan archivos de meets para sincronización con CRM',
  20. copy=False,
  21. groups='base.group_system'
  22. )
  23. @api.model
  24. def _get_fields_blacklist(self):
  25. """Get list of google drive fields that won't be formatted in session_info."""
  26. google_fields_blacklist = [
  27. 'google_rtoken',
  28. 'google_token',
  29. 'google_token_validity',
  30. 'google_email',
  31. 'google_sync_enabled',
  32. 'google_crm_meets_folder_id'
  33. ]
  34. return super()._get_fields_blacklist() + google_fields_blacklist
  35. def _set_google_auth_tokens(self, access_token, refresh_token, expires_at):
  36. """Set Google authentication tokens for the user"""
  37. self.sudo().write({
  38. 'google_rtoken': refresh_token,
  39. 'google_token': access_token,
  40. 'google_token_validity': expires_at if expires_at else False,
  41. })
  42. def _google_authenticated(self):
  43. """Check if user is authenticated with Google"""
  44. self.ensure_one()
  45. # Verificar que tenemos tanto refresh token como access token
  46. return bool(self.sudo().google_rtoken and self.sudo().google_token)
  47. def _is_google_token_valid(self):
  48. """Check if Google token is still valid"""
  49. self.ensure_one()
  50. return (self.sudo().google_token_validity and
  51. self.sudo().google_token_validity >= (fields.Datetime.now() + timedelta(minutes=1)))
  52. def _refresh_google_token(self):
  53. """Refresh Google access token using refresh token"""
  54. self.ensure_one()
  55. try:
  56. access_token, ttl = self.env['google.service']._refresh_google_token('drive', self.sudo().google_rtoken)
  57. self.sudo().write({
  58. 'google_token': access_token,
  59. 'google_token_validity': fields.Datetime.now() + timedelta(seconds=ttl),
  60. })
  61. _logger.info(f"Google token refreshed for user {self.user_id.name}")
  62. return True
  63. except Exception as e:
  64. _logger.error(f"Failed to refresh Google token for user {self.user_id.name}: {str(e)}")
  65. # Solo eliminar tokens si es definitivamente un error de credenciales inválidas
  66. if 'invalid_grant' in str(e) or 'invalid_token' in str(e):
  67. _logger.warning(f"Invalid refresh token for user {self.user_id.name}, deleting tokens")
  68. self.env.cr.rollback()
  69. self.sudo()._set_google_auth_tokens(False, False, False)
  70. self.env.cr.commit()
  71. return False
  72. else:
  73. # Para otros errores (timeout, red, etc.), mantener los tokens y reintentar después
  74. _logger.warning(f"Temporary error refreshing token for user {self.user_id.name}, keeping tokens for retry")
  75. return False
  76. def _get_google_access_token(self):
  77. """Get a valid Google access token, refreshing if necessary"""
  78. self.ensure_one()
  79. if not self._google_authenticated():
  80. return None
  81. # Si el token no es válido, intentar renovarlo
  82. if not self._is_google_token_valid():
  83. _logger.info(f"Token expired for user {self.user_id.name}, attempting refresh")
  84. # Intentar renovar el token
  85. if not self._refresh_google_token():
  86. _logger.warning(f"Token refresh failed for user {self.user_id.name}")
  87. # Si falla la renovación, verificar si aún tenemos refresh token
  88. if not self.sudo().google_rtoken:
  89. _logger.error(f"No refresh token available for user {self.user_id.name}")
  90. return None
  91. # Intentar una vez más con un pequeño delay
  92. import time
  93. time.sleep(1)
  94. _logger.info(f"Retrying token refresh for user {self.user_id.name}")
  95. if not self._refresh_google_token():
  96. _logger.error(f"Final token refresh attempt failed for user {self.user_id.name}")
  97. return None
  98. # Verificar que tenemos un token válido
  99. token = self.sudo().google_token
  100. if not token:
  101. _logger.warning(f"No access token available for user {self.user_id.name}")
  102. return None
  103. return token
  104. def _clean_invalid_tokens(self):
  105. """Clean invalid tokens when they can't be refreshed"""
  106. self.ensure_one()
  107. _logger.warning(f"Cleaning invalid tokens for user {self.user_id.name}")
  108. self.sudo()._set_google_auth_tokens(False, False, False)
  109. self.sudo().write({
  110. 'google_email': False,
  111. 'google_sync_enabled': False,
  112. })
  113. def action_connect_google(self):
  114. """Connect user's Google account"""
  115. self.ensure_one()
  116. # Get Google API credentials from system settings
  117. config = self.env['ir.config_parameter'].sudo()
  118. client_id = config.get_param('google_api.client_id', '')
  119. client_secret = config.get_param('google_api.client_secret', '')
  120. if not client_id or not client_secret:
  121. _logger.error("Google API credentials not configured.")
  122. raise UserError(_('Google API credentials not configured. Please contact your administrator.'))
  123. # Build OAuth authorization URL
  124. base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
  125. redirect_uri = f"{base_url}/web/google_oauth_callback"
  126. scope = 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/calendar'
  127. # Generate the authorization URL using our own credentials
  128. authorize_uri = self._get_google_authorize_uri(
  129. client_id=client_id,
  130. redirect_uri=redirect_uri,
  131. scope=scope,
  132. state=str(self.id) # Pass user settings ID as state
  133. )
  134. return {
  135. 'type': 'ir.actions.act_url',
  136. 'url': authorize_uri,
  137. 'target': 'new',
  138. }
  139. def action_disconnect_google(self):
  140. """Disconnect user's Google account"""
  141. self.ensure_one()
  142. self.sudo()._set_google_auth_tokens(False, False, False)
  143. self.sudo().write({
  144. 'google_email': False,
  145. 'google_sync_enabled': False,
  146. })
  147. return {
  148. 'type': 'ir.actions.client',
  149. 'tag': 'display_notification',
  150. 'params': {
  151. 'title': _('Success'),
  152. 'message': _('Google account disconnected successfully.'),
  153. 'type': 'success',
  154. 'sticky': False,
  155. }
  156. }
  157. def action_test_google_connection(self):
  158. """Test Google connection for the user"""
  159. self.ensure_one()
  160. if not self._google_authenticated():
  161. raise UserError(_('Google account not connected. Please connect your account first.'))
  162. access_token = self._get_google_access_token()
  163. if not access_token:
  164. raise UserError(_('Could not obtain valid access token. Please reconnect your Google account.'))
  165. try:
  166. # Test Google Drive API access
  167. headers = {
  168. 'Authorization': f'Bearer {access_token}',
  169. 'Content-Type': 'application/json'
  170. }
  171. response = self.env['google.drive.service']._do_request(
  172. '/drive/v3/about',
  173. params={'fields': 'user'},
  174. headers=headers
  175. )
  176. if response and 'user' in response:
  177. user_email = response['user'].get('emailAddress', 'Unknown')
  178. self.sudo().write({'google_email': user_email})
  179. return {
  180. 'type': 'ir.actions.client',
  181. 'tag': 'display_notification',
  182. 'params': {
  183. 'title': _('Success'),
  184. 'message': _('Google connection successful! Connected as: %s') % user_email,
  185. 'type': 'success',
  186. 'sticky': False,
  187. }
  188. }
  189. else:
  190. raise UserError(_('Could not retrieve user information from Google.'))
  191. except Exception as e:
  192. _logger.error(f"Google connection test failed for user {self.user_id.name}: {str(e)}")
  193. raise UserError(_('Google connection test failed: %s') % str(e))
  194. def _get_google_authorize_uri(self, client_id, redirect_uri, scope, state):
  195. """Generate Google OAuth authorization URI using our own credentials"""
  196. import urllib.parse
  197. params = {
  198. 'response_type': 'code',
  199. 'client_id': client_id,
  200. 'redirect_uri': redirect_uri,
  201. 'scope': scope,
  202. 'state': state,
  203. 'access_type': 'offline',
  204. 'prompt': 'consent select_account', # Force account selection
  205. 'hd': '', # Allow any hosted domain
  206. 'include_granted_scopes': 'true'
  207. }
  208. base_url = 'https://accounts.google.com/o/oauth2/auth'
  209. query_string = urllib.parse.urlencode(params)
  210. return f"{base_url}?{query_string}"
  211. def _exchange_authorization_code_for_tokens(self, code, client_id, client_secret, redirect_uri):
  212. """Exchange authorization code for access and refresh tokens"""
  213. import requests
  214. token_url = 'https://oauth2.googleapis.com/token'
  215. data = {
  216. 'code': code,
  217. 'client_id': client_id,
  218. 'client_secret': client_secret,
  219. 'redirect_uri': redirect_uri,
  220. 'grant_type': 'authorization_code'
  221. }
  222. response = requests.post(token_url, data=data)
  223. if response.status_code != 200:
  224. _logger.error(f"Token exchange failed: {response.status_code} - {response.text}")
  225. raise UserError(_('Failed to exchange authorization code for tokens: %s') % response.text)
  226. token_data = response.json()
  227. access_token = token_data.get('access_token')
  228. refresh_token = token_data.get('refresh_token')
  229. expires_in = token_data.get('expires_in', 3600)
  230. if not access_token:
  231. raise UserError(_('No access token received from Google'))
  232. # Calculate expiration time
  233. from datetime import datetime, timedelta
  234. expires_at = datetime.now() + timedelta(seconds=expires_in)
  235. return access_token, refresh_token, expires_at