res_users_settings.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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. return bool(self.sudo().google_rtoken)
  46. def _is_google_token_valid(self):
  47. """Check if Google token is still valid"""
  48. self.ensure_one()
  49. return (self.sudo().google_token_validity and
  50. self.sudo().google_token_validity >= (fields.Datetime.now() + timedelta(minutes=1)))
  51. def _refresh_google_token(self):
  52. """Refresh Google access token using refresh token"""
  53. self.ensure_one()
  54. try:
  55. access_token, ttl = self.env['google.service']._refresh_google_token('drive', self.sudo().google_rtoken)
  56. self.sudo().write({
  57. 'google_token': access_token,
  58. 'google_token_validity': fields.Datetime.now() + timedelta(seconds=ttl),
  59. })
  60. _logger.info(f"Google token refreshed for user {self.user_id.name}")
  61. return True
  62. except Exception as e:
  63. _logger.error(f"Failed to refresh Google token for user {self.user_id.name}: {str(e)}")
  64. # Solo eliminar tokens si es definitivamente un error de credenciales inválidas
  65. if 'invalid_grant' in str(e) or 'invalid_token' in str(e):
  66. _logger.warning(f"Invalid refresh token for user {self.user_id.name}, deleting tokens")
  67. self.env.cr.rollback()
  68. self.sudo()._set_google_auth_tokens(False, False, False)
  69. self.env.cr.commit()
  70. return False
  71. else:
  72. # Para otros errores (timeout, red, etc.), mantener los tokens y reintentar después
  73. _logger.warning(f"Temporary error refreshing token for user {self.user_id.name}, keeping tokens for retry")
  74. return False
  75. def _get_google_access_token(self):
  76. """Get a valid Google access token, refreshing if necessary"""
  77. self.ensure_one()
  78. if not self._google_authenticated():
  79. return None
  80. if not self._is_google_token_valid():
  81. if not self._refresh_google_token():
  82. return None
  83. return self.sudo().google_token
  84. def action_connect_google(self):
  85. """Connect user's Google account"""
  86. self.ensure_one()
  87. # Get Google API credentials from system settings
  88. config = self.env['ir.config_parameter'].sudo()
  89. client_id = config.get_param('google_api.client_id', '')
  90. client_secret = config.get_param('google_api.client_secret', '')
  91. if not client_id or not client_secret:
  92. _logger.error("Google API credentials not configured.")
  93. raise UserError(_('Google API credentials not configured. Please contact your administrator.'))
  94. # Build OAuth authorization URL
  95. base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
  96. redirect_uri = f"{base_url}/web/google_oauth_callback"
  97. scope = 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/calendar'
  98. # Generate the authorization URL using our own credentials
  99. authorize_uri = self._get_google_authorize_uri(
  100. client_id=client_id,
  101. redirect_uri=redirect_uri,
  102. scope=scope,
  103. state=str(self.id) # Pass user settings ID as state
  104. )
  105. return {
  106. 'type': 'ir.actions.act_url',
  107. 'url': authorize_uri,
  108. 'target': 'new',
  109. }
  110. def action_disconnect_google(self):
  111. """Disconnect user's Google account"""
  112. self.ensure_one()
  113. self.sudo()._set_google_auth_tokens(False, False, False)
  114. self.sudo().write({
  115. 'google_email': False,
  116. 'google_sync_enabled': False,
  117. })
  118. return {
  119. 'type': 'ir.actions.client',
  120. 'tag': 'display_notification',
  121. 'params': {
  122. 'title': _('Success'),
  123. 'message': _('Google account disconnected successfully.'),
  124. 'type': 'success',
  125. 'sticky': False,
  126. }
  127. }
  128. def action_test_google_connection(self):
  129. """Test Google connection for the user"""
  130. self.ensure_one()
  131. if not self._google_authenticated():
  132. raise UserError(_('Google account not connected. Please connect your account first.'))
  133. access_token = self._get_google_access_token()
  134. if not access_token:
  135. raise UserError(_('Could not obtain valid access token. Please reconnect your Google account.'))
  136. try:
  137. # Test Google Drive API access
  138. headers = {
  139. 'Authorization': f'Bearer {access_token}',
  140. 'Content-Type': 'application/json'
  141. }
  142. response = self.env['google.drive.service']._do_request(
  143. '/drive/v3/about',
  144. params={'fields': 'user'},
  145. headers=headers
  146. )
  147. if response and 'user' in response:
  148. user_email = response['user'].get('emailAddress', 'Unknown')
  149. self.sudo().write({'google_email': user_email})
  150. return {
  151. 'type': 'ir.actions.client',
  152. 'tag': 'display_notification',
  153. 'params': {
  154. 'title': _('Success'),
  155. 'message': _('Google connection successful! Connected as: %s') % user_email,
  156. 'type': 'success',
  157. 'sticky': False,
  158. }
  159. }
  160. else:
  161. raise UserError(_('Could not retrieve user information from Google.'))
  162. except Exception as e:
  163. _logger.error(f"Google connection test failed for user {self.user_id.name}: {str(e)}")
  164. raise UserError(_('Google connection test failed: %s') % str(e))
  165. def _get_google_authorize_uri(self, client_id, redirect_uri, scope, state):
  166. """Generate Google OAuth authorization URI using our own credentials"""
  167. import urllib.parse
  168. params = {
  169. 'response_type': 'code',
  170. 'client_id': client_id,
  171. 'redirect_uri': redirect_uri,
  172. 'scope': scope,
  173. 'state': state,
  174. 'access_type': 'offline',
  175. 'prompt': 'consent select_account', # Force account selection
  176. 'hd': '', # Allow any hosted domain
  177. 'include_granted_scopes': 'true'
  178. }
  179. base_url = 'https://accounts.google.com/o/oauth2/auth'
  180. query_string = urllib.parse.urlencode(params)
  181. return f"{base_url}?{query_string}"
  182. def _exchange_authorization_code_for_tokens(self, code, client_id, client_secret, redirect_uri):
  183. """Exchange authorization code for access and refresh tokens"""
  184. import requests
  185. token_url = 'https://oauth2.googleapis.com/token'
  186. data = {
  187. 'code': code,
  188. 'client_id': client_id,
  189. 'client_secret': client_secret,
  190. 'redirect_uri': redirect_uri,
  191. 'grant_type': 'authorization_code'
  192. }
  193. response = requests.post(token_url, data=data)
  194. if response.status_code != 200:
  195. _logger.error(f"Token exchange failed: {response.status_code} - {response.text}")
  196. raise UserError(_('Failed to exchange authorization code for tokens: %s') % response.text)
  197. token_data = response.json()
  198. access_token = token_data.get('access_token')
  199. refresh_token = token_data.get('refresh_token')
  200. expires_in = token_data.get('expires_in', 3600)
  201. if not access_token:
  202. raise UserError(_('No access token received from Google'))
  203. # Calculate expiration time
  204. from datetime import datetime, timedelta
  205. expires_at = datetime.now() + timedelta(seconds=expires_in)
  206. return access_token, refresh_token, expires_at