res_users_settings.py 9.5 KB

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