소스 검색

Merge commit '57a01f55f03d2c2b9448bb01e1412c65e3fe7439' into TC

root 5 달 전
부모
커밋
0eb3bd0d27

+ 3 - 0
google_api/__manifest__.py

@@ -14,9 +14,12 @@
     'depends': [
         'base',
         'base_setup',
+        'google_account',
+        'google_calendar',
     ],
     'data': [
         'views/res_config_settings_views.xml',
+        'views/res_users_views.xml',
     ],
     'installable': True,
     'application': False,

+ 36 - 63
google_api/controllers/main.py

@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
-import requests
 import json
 from odoo import http
 from odoo.http import request
@@ -11,7 +10,7 @@ class GoogleOAuthController(http.Controller):
     
     @http.route('/web/google_oauth_callback', type='http', auth='public', website=True)
     def google_oauth_callback(self, **kw):
-        """Handle Google OAuth callback and exchange code for token"""
+        """Handle Google OAuth callback for Google Drive integration"""
         
         # Get the authorization code from the callback
         code = kw.get('code')
@@ -43,73 +42,47 @@ class GoogleOAuthController(http.Controller):
             """
         
         try:
-            # Get configuration
-            client_id = request.env['ir.config_parameter'].sudo().get_param('google_api.client_id')
-            client_secret = request.env['ir.config_parameter'].sudo().get_param('google_api.client_secret')
-            redirect_uri = request.env['ir.config_parameter'].sudo().get_param('google_api.manual_redirect_uri')
+            # Get user settings ID from state (we pass the user settings ID as state)
+            user_settings_id = int(state) if state else None
             
-            if not all([client_id, client_secret, redirect_uri]):
-                raise Exception("Missing OAuth configuration")
+            if not user_settings_id:
+                raise ValueError("No user settings ID provided in state")
             
-            # Exchange code for token
-            token_url = "https://oauth2.googleapis.com/token"
-            token_data = {
-                'client_id': client_id,
-                'client_secret': client_secret,
-                'code': code,
-                'grant_type': 'authorization_code',
-                'redirect_uri': redirect_uri,
-            }
+            # Get user settings record
+            user_settings = request.env['res.users.settings'].sudo().browse(user_settings_id)
+            if not user_settings.exists():
+                raise ValueError(f"User settings with ID {user_settings_id} not found")
             
-            response = requests.post(token_url, data=token_data, timeout=30)
+            # Use Odoo's standard Google service for token exchange
+            base_url = request.httprequest.url_root.strip('/') or request.env.user.get_base_url()
+            redirect_uri = f'{base_url}/web/google_oauth_callback'
             
-            if response.status_code != 200:
-                raise Exception(f"Token exchange failed: {response.status_code} - {response.text}")
+            # Get Google API credentials from system settings
+            config = request.env['ir.config_parameter'].sudo()
+            client_id = config.get_param('google_api.client_id', '')
+            client_secret = config.get_param('google_api.client_secret', '')
             
-            token_info = response.json()
-            
-            # Store the token information
-            access_token = token_info.get('access_token')
-            refresh_token = token_info.get('refresh_token')
-            
-            if not access_token:
-                raise Exception("No access token received")
-            
-            # Store tokens in config parameters (for now, in production you'd want a more secure storage)
-            request.env['ir.config_parameter'].sudo().set_param('google_api.access_token', access_token)
-            if refresh_token:
-                request.env['ir.config_parameter'].sudo().set_param('google_api.refresh_token', refresh_token)
-            
-            # Test the token by making a simple API call
-            headers = {'Authorization': f'Bearer {access_token}'}
-            test_response = requests.get(
-                'https://www.googleapis.com/drive/v3/about?fields=user',
-                headers=headers,
-                timeout=10
+            # Exchange authorization code for tokens using our own method
+            access_token, refresh_token, expires_at = user_settings._exchange_authorization_code_for_tokens(
+                code,
+                client_id,
+                client_secret,
+                redirect_uri
             )
             
-            if test_response.status_code == 200:
-                user_info = test_response.json()
-                user_email = user_info.get('user', {}).get('emailAddress', 'Unknown')
-                
-                return f"""
-                <html>
-                <head><title>OAuth Success</title></head>
-                <body>
-                    <h1>✅ Google OAuth Success!</h1>
-                    <p>Successfully connected to Google Drive as: <strong>{user_email}</strong></p>
-                    <p>Access token has been stored and is ready to use.</p>
-                    <p><a href="/web#action=google_api.action_google_api_settings">Return to Google API Settings</a></p>
-                    <script>
-                        setTimeout(function() {{
-                            window.close();
-                        }}, 3000);
-                    </script>
-                </body>
-                </html>
-                """
-            else:
-                raise Exception(f"Token validation failed: {test_response.status_code}")
+            # Store tokens in user settings
+            user_settings._set_google_auth_tokens(access_token, refresh_token, expires_at)
+            
+            return f"""
+            <html>
+            <head><title>OAuth Success</title></head>
+            <body>
+                <h1>✅ Google Connect Success!</h1>
+                <p>Successfully connected to Google services!</p>
+                <p><a href="/web">Return to Odoo</a></p>
+            </body>
+            </html>
+            """
                 
         except Exception as e:
             return f"""
@@ -118,7 +91,7 @@ class GoogleOAuthController(http.Controller):
             <body>
                 <h1>❌ OAuth Error</h1>
                 <p>Error: {str(e)}</p>
-                <p><a href="/web#action=google_api.action_google_api_settings">Return to Google API Settings</a></p>
+                <p><a href="/web">Return to Odoo</a></p>
             </body>
             </html>
             """

+ 6 - 0
google_api/models/__init__.py

@@ -1 +1,7 @@
 from . import res_config_settings
+from . import res_users_settings
+from . import res_users
+from . import google_drive_service
+from . import google_auth_service
+from . import google_workspace_service
+from . import google_calendar_service

+ 176 - 0
google_api/models/google_auth_service.py

@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+
+import logging
+import requests
+from odoo import models, api
+from odoo.exceptions import UserError
+from odoo.tools.translate import _
+
+_logger = logging.getLogger(__name__)
+
+
+class GoogleAuthService(models.AbstractModel):
+    _name = 'google.auth.service'
+    _description = 'Google Authentication Service'
+
+    def test_connection(self):
+        """Test Google Drive API connection"""
+        try:
+            access_token = self._get_access_token()
+            if not access_token:
+                return False
+
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+
+            response = requests.get(
+                'https://www.googleapis.com/drive/v3/about',
+                headers=headers,
+                params={'fields': 'user'},
+                timeout=10
+            )
+
+            if response.status_code == 200:
+                return True
+            elif response.status_code == 401:
+                _logger.warning("Google Drive access token is invalid (401)")
+                # Try to refresh the token
+                if self.refresh_access_token():
+                    return self.test_connection()
+                return False
+            else:
+                _logger.warning(f"Google Drive API test failed with status {response.status_code}")
+                return False
+
+        except requests.exceptions.Timeout:
+            _logger.error("Google Drive API test timeout")
+            return False
+        except requests.exceptions.ConnectionError:
+            _logger.error("Google Drive API connection error")
+            return False
+        except Exception as e:
+            _logger.error(f"Google Drive API test error: {str(e)}")
+            return False
+
+    def refresh_access_token(self):
+        """Refresh the access token using the refresh token"""
+        try:
+            refresh_token = self.env['ir.config_parameter'].sudo().get_param('google_api.refresh_token')
+            client_id = self.env['ir.config_parameter'].sudo().get_param('google_api.client_id')
+            client_secret = self.env['ir.config_parameter'].sudo().get_param('google_api.client_secret')
+
+            if not all([refresh_token, client_id, client_secret]):
+                _logger.error("Missing OAuth2 credentials")
+                return False
+
+            # Exchange refresh token for new access token
+            token_url = 'https://oauth2.googleapis.com/token'
+            data = {
+                'client_id': client_id,
+                'client_secret': client_secret,
+                'refresh_token': refresh_token,
+                'grant_type': 'refresh_token'
+            }
+
+            response = requests.post(token_url, data=data, timeout=30)
+
+            if response.status_code == 200:
+                token_data = response.json()
+                new_access_token = token_data.get('access_token')
+
+                if new_access_token:
+                    # Store the new access token
+                    self.env['ir.config_parameter'].sudo().set_param('google_api.access_token', new_access_token)
+                    _logger.info("Access token refreshed successfully")
+                    return True
+                else:
+                    _logger.error("No access token in response")
+                    return False
+            else:
+                _logger.error(f"Failed to refresh token: {response.status_code} - {response.text}")
+                return False
+
+        except Exception as e:
+            _logger.error(f"Failed to refresh access token: {str(e)}")
+            return False
+
+    def get_access_token(self, user=None):
+        """Get a valid access token for the specified user, refreshing if necessary"""
+        if not user:
+            user = self.env.user
+        
+        try:
+            # Get user's Google Drive settings
+            user_settings = user.res_users_settings_id
+            if not user_settings:
+                return None
+            
+            # Get access token from user settings
+            return user_settings._get_google_access_token()
+            
+        except Exception as e:
+            _logger.error(f"Error getting access token for user {user.name}: {str(e)}")
+            return None
+
+    def _test_token_validity(self, access_token):
+        """Test if an access token is still valid"""
+        try:
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+
+            response = requests.get(
+                'https://www.googleapis.com/drive/v3/about',
+                headers=headers,
+                params={'fields': 'user'},
+                timeout=5
+            )
+
+            return response.status_code == 200
+
+        except Exception as e:
+            _logger.error(f"Error testing token validity: {str(e)}")
+            return False
+
+    def validate_credentials(self):
+        """Validate that all required OAuth2 credentials are configured"""
+        try:
+            client_id = self.env['ir.config_parameter'].sudo().get_param('google_api.client_id')
+            client_secret = self.env['ir.config_parameter'].sudo().get_param('google_api.client_secret')
+            refresh_token = self.env['ir.config_parameter'].sudo().get_param('google_api.refresh_token')
+
+            missing_credentials = []
+            if not client_id:
+                missing_credentials.append('Client ID')
+            if not client_secret:
+                missing_credentials.append('Client Secret')
+            if not refresh_token:
+                missing_credentials.append('Refresh Token')
+
+            if missing_credentials:
+                raise UserError(_('Missing Google API credentials: %s') % ', '.join(missing_credentials))
+
+            return True
+
+        except Exception as e:
+            _logger.error(f"Error validating credentials: {str(e)}")
+            raise
+
+    def get_auth_headers(self, user=None):
+        """Get authentication headers for API requests"""
+        try:
+            access_token = self.get_access_token(user)
+            if not access_token:
+                raise UserError(_('Could not obtain valid access token for user %s') % (user.name if user else self.env.user.name))
+
+            return {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+
+        except Exception as e:
+            _logger.error(f"Error getting auth headers: {str(e)}")
+            raise

+ 539 - 0
google_api/models/google_calendar_service.py

@@ -0,0 +1,539 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+from odoo import models, _
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+
+class GoogleCalendarService(models.AbstractModel):
+    _name = 'google.calendar.service'
+    _description = 'Google Calendar Service'
+
+    def _get_credentials(self, user=None):
+        """Obtener credenciales de configuración para el usuario especificado"""
+        if not user:
+            user = self.env.user
+        
+        config = self.env['ir.config_parameter'].sudo()
+        
+        enabled = config.get_param('google_api.enabled', 'False')
+        if not enabled or enabled == 'False':
+            raise UserError(_('Google API Integration is not enabled'))
+        
+        client_id = config.get_param('google_api.client_id', '')
+        client_secret = config.get_param('google_api.client_secret', '')
+        
+        if not client_id or not client_secret:
+            raise UserError(_('Google API credentials not configured'))
+        
+        # Get access token from user settings
+        user_settings = user.res_users_settings_id
+        if not user_settings or not user_settings._google_authenticated():
+            raise UserError(_('User %s is not authenticated with Google. Please connect your account first.') % user.name)
+        
+        access_token = user_settings._get_google_access_token()
+        if not access_token:
+            raise UserError(_('Could not obtain valid access token for user %s. Please reconnect your Google account.') % user.name)
+        
+        return {
+            'client_id': client_id,
+            'client_secret': client_secret,
+            'access_token': access_token
+        }
+
+    def _do_request(self, endpoint, method='GET', params=None, json_data=None, headers=None):
+        """Make a request to Google Calendar API"""
+        import requests
+        
+        base_url = 'https://www.googleapis.com/calendar/v3'
+        url = f"{base_url}{endpoint}"
+        
+        if headers is None:
+            headers = {}
+        
+        try:
+            # Get credentials (includes access token)
+            credentials = self._get_credentials()
+            
+            # Add authorization header
+            headers['Authorization'] = f'Bearer {credentials["access_token"]}'
+            
+            if method == 'GET':
+                response = requests.get(url, params=params, headers=headers)
+            elif method == 'POST':
+                response = requests.post(url, params=params, json=json_data, headers=headers)
+            elif method == 'PUT':
+                response = requests.put(url, params=params, json=json_data, headers=headers)
+            elif method == 'DELETE':
+                response = requests.delete(url, params=params, headers=headers)
+            else:
+                raise UserError(_('Unsupported HTTP method: %s') % method)
+            
+            response.raise_for_status()
+            
+            if response.status_code == 204:  # No content
+                return None
+            
+            return response.json()
+            
+        except requests.exceptions.RequestException as e:
+            _logger.error(f"Google Calendar API request failed: {str(e)}")
+            raise UserError(_('Google Calendar API Error: %s') % str(e))
+
+    def get_meetings_with_recordings(self, days_back=15):
+        """Get Google Meet meetings with recordings from the last N days"""
+        try:
+            from datetime import datetime, timedelta
+            
+            # Calculate date range - search for PAST meetings (not future)
+            end_date = datetime.now()
+            start_date = end_date - timedelta(days=days_back)
+            
+            _logger.info(f"Searching for Google Meet events with recordings from {start_date} to {end_date}")
+            
+            # Get calendar events with conference data (Google Meet events)
+            # Use timeMin and timeMax to search for PAST events only
+            time_min = start_date.isoformat() + 'Z'
+            time_max = end_date.isoformat() + 'Z'
+            
+            # Get all events with pagination to ensure we get everything
+            all_events = []
+            page_token = None
+            
+            while True:
+                params = {
+                    'timeMin': time_min,
+                    'timeMax': time_max,
+                    'singleEvents': 'true',
+                    'orderBy': 'startTime',
+                    'maxResults': 2500,  # Maximum allowed by Google Calendar API
+                    'fields': 'items(id,summary,start,end,attendees,conferenceData,hangoutLink),nextPageToken'
+                }
+                
+                if page_token:
+                    params['pageToken'] = page_token
+                
+                response = self._do_request('/calendars/primary/events', params=params)
+                
+                if 'items' in response:
+                    all_events.extend(response['items'])
+                
+                # Check if there are more pages
+                page_token = response.get('nextPageToken')
+                if not page_token:
+                    break
+            
+            events = {'items': all_events}
+            _logger.info(f"Retrieved {len(all_events)} total events from Google Calendar API")
+            
+            # Filter out future events - only process events that have already ended
+            from datetime import timezone
+            current_time = datetime.now(timezone.utc)
+            past_events = []
+            
+            for event in events.get('items', []):
+                end_time_str = event['end'].get('dateTime', event['end'].get('date'))
+                if 'T' in end_time_str:
+                    event_end = datetime.fromisoformat(end_time_str.replace('Z', '+00:00'))
+                else:
+                    # For date-only events, assume they end at 23:59 UTC
+                    event_end = datetime.fromisoformat(end_time_str + 'T23:59:59+00:00')
+                
+                # Only include events that have already ended
+                if event_end < current_time:
+                    past_events.append(event)
+                else:
+                    _logger.info(f"Skipping future event: {event.get('summary', 'Unknown')} (ends: {event_end})")
+            
+            _logger.info(f"Found {len(past_events)} past events out of {len(events.get('items', []))} total events")
+            
+            events = {'items': past_events}
+            
+            meetings_with_recordings = []
+            
+            for event in events.get('items', []):
+                # Check if it's a Google Meet event
+                if self._is_google_meet_event(event):
+                    event_title = event.get('summary', 'Sin título')
+                    hangout_link = event.get('hangoutLink')
+                    
+                    # Get meeting date and time
+                    start_time = event['start'].get('dateTime', event['start'].get('date'))
+                    end_time = event['end'].get('dateTime', event['end'].get('date'))
+                    
+                    # Format the date for display
+                    from datetime import datetime
+                    if 'T' in start_time:
+                        meeting_date = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
+                        formatted_date = meeting_date.strftime('%Y-%m-%d %H:%M')
+                    else:
+                        meeting_date = datetime.fromisoformat(start_time)
+                        formatted_date = meeting_date.strftime('%Y-%m-%d')
+                    
+                    _logger.info(f"Found Google Meet event: {event_title} ({formatted_date})")
+                    
+                    # Try to find ALL files associated with this meeting
+                    meeting_files = self._find_recordings_for_meeting(event, event_title)
+                    
+                    if meeting_files:
+                        participants = self._extract_participants(event)
+                        
+                        meeting_data = {
+                            'id': event['id'],
+                            'title': event_title,
+                            'start_time': event['start'].get('dateTime', event['start'].get('date')),
+                            'end_time': event['end'].get('dateTime', event['end'].get('date')),
+                            'participants': participants,
+                            'recording_files': meeting_files,
+                            'hangout_link': hangout_link,
+                            'calendar_event': event
+                        }
+                        meetings_with_recordings.append(meeting_data)
+                        _logger.info(f"✅ Found meeting with files: {event_title} ({formatted_date}) - Participants: {len(participants)}, Files: {len(meeting_files)})")
+                    else:
+                        _logger.info(f"❌ No files found for meeting: {event_title}")
+            
+            _logger.info(f"Total meetings with recordings found: {len(meetings_with_recordings)}")
+            return meetings_with_recordings
+            
+        except Exception as e:
+            _logger.error(f"Failed to get meetings with recordings: {str(e)}")
+            return []
+
+    def get_meetings_with_recordings_for_date(self, target_date):
+        """Get Google Meet meetings with recordings from a specific date
+        
+        Args:
+            target_date: datetime.date object representing the target date
+        """
+        try:
+            from datetime import datetime, timedelta
+            
+            # Convert target_date to datetime range for that specific day
+            start_date = datetime.combine(target_date, datetime.min.time())
+            end_date = datetime.combine(target_date, datetime.max.time())
+            
+            _logger.info(f"Searching for Google Meet events with recordings for specific date: {target_date} ({start_date} to {end_date})")
+            
+            # Get calendar events with conference data (Google Meet events)
+            # Use timeMin and timeMax to search for events on the specific date
+            time_min = start_date.isoformat() + 'Z'
+            time_max = end_date.isoformat() + 'Z'
+            
+            # Get all events with pagination to ensure we get everything
+            all_events = []
+            page_token = None
+            
+            while True:
+                params = {
+                    'timeMin': time_min,
+                    'timeMax': time_max,
+                    'singleEvents': 'true',
+                    'orderBy': 'startTime',
+                    'maxResults': 2500,  # Maximum allowed by Google Calendar API
+                    'fields': 'items(id,summary,start,end,attendees,conferenceData,hangoutLink),nextPageToken'
+                }
+                
+                if page_token:
+                    params['pageToken'] = page_token
+                
+                response = self._do_request('/calendars/primary/events', params=params)
+                
+                if 'items' in response:
+                    all_events.extend(response['items'])
+                
+                # Check if there are more pages
+                page_token = response.get('nextPageToken')
+                if not page_token:
+                    break
+            
+            events = {'items': all_events}
+            _logger.info(f"Retrieved {len(all_events)} total events from Google Calendar API for date {target_date}")
+            
+            # Filter out future events - only process events that have already ended
+            from datetime import timezone
+            current_time = datetime.now(timezone.utc)
+            past_events = []
+            
+            for event in events.get('items', []):
+                end_time_str = event['end'].get('dateTime', event['end'].get('date'))
+                if 'T' in end_time_str:
+                    event_end = datetime.fromisoformat(end_time_str.replace('Z', '+00:00'))
+                else:
+                    # For date-only events, assume they end at 23:59 UTC
+                    event_end = datetime.fromisoformat(end_time_str + 'T23:59:59+00:00')
+                
+                # Only include events that have already ended
+                if event_end < current_time:
+                    past_events.append(event)
+                else:
+                    _logger.info(f"Skipping future event: {event.get('summary', 'Unknown')} (ends: {event_end})")
+            
+            _logger.info(f"Found {len(past_events)} past events out of {len(events.get('items', []))} total events for date {target_date}")
+            
+            events = {'items': past_events}
+            
+            meetings_with_recordings = []
+            
+            for event in events.get('items', []):
+                # Check if it's a Google Meet event
+                if self._is_google_meet_event(event):
+                    event_title = event.get('summary', 'Sin título')
+                    hangout_link = event.get('hangoutLink')
+                    
+                    # Get meeting date and time
+                    start_time = event['start'].get('dateTime', event['start'].get('date'))
+                    end_time = event['end'].get('dateTime', event['end'].get('date'))
+                    
+                    # Format the date for display
+                    from datetime import datetime
+                    if 'T' in start_time:
+                        meeting_date = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
+                        formatted_date = meeting_date.strftime('%Y-%m-%d %H:%M')
+                    else:
+                        meeting_date = datetime.fromisoformat(start_time)
+                        formatted_date = meeting_date.strftime('%Y-%m-%d')
+                    
+                    _logger.info(f"Found Google Meet event: {event_title} ({formatted_date})")
+                    
+                    # Try to find ALL files associated with this meeting
+                    meeting_files = self._find_recordings_for_meeting(event, event_title)
+                    
+                    if meeting_files:
+                        participants = self._extract_participants(event)
+                        
+                        meeting_data = {
+                            'id': event['id'],
+                            'title': event_title,
+                            'start_time': event['start'].get('dateTime', event['start'].get('date')),
+                            'end_time': event['end'].get('dateTime', event['end'].get('date')),
+                            'participants': participants,
+                            'recording_files': meeting_files,
+                            'hangout_link': hangout_link,
+                            'calendar_event': event
+                        }
+                        meetings_with_recordings.append(meeting_data)
+                        _logger.info(f"✅ Found meeting with files: {event_title} ({formatted_date}) - Participants: {len(participants)}, Files: {len(meeting_files)})")
+                    else:
+                        _logger.info(f"❌ No files found for meeting: {event_title}")
+            
+            _logger.info(f"Total meetings with recordings found for date {target_date}: {len(meetings_with_recordings)}")
+            return meetings_with_recordings
+            
+        except Exception as e:
+            _logger.error(f"Failed to get meetings with recordings for date {target_date}: {str(e)}")
+            return []
+
+    def _find_recordings_for_meeting(self, event, event_title):
+        """Find ALL files associated with a specific meeting (videos, documents, etc.)"""
+        try:
+            from datetime import datetime, timedelta
+            
+            drive_service = self.env['google.drive.service']
+            
+            # Search for video files that might be recordings for this meeting
+            # Use the meeting title and date to find relevant files
+            start_date = event['start'].get('dateTime', event['start'].get('date'))
+            
+            # Convert to datetime for search
+            if 'T' in start_date:
+                meeting_date = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
+            else:
+                meeting_date = datetime.fromisoformat(start_date)
+            
+            # Search for files created around the meeting time (±1 day)
+            search_start = (meeting_date - timedelta(days=1)).isoformat()
+            search_end = (meeting_date + timedelta(days=1)).isoformat()
+            
+            # Search for ALL file types that might be associated with the meeting
+            # This includes videos, documents, presentations, transcripts, etc.
+            files = drive_service._do_request(
+                '/drive/v3/files',
+                params={
+                    'q': f"createdTime > '{search_start}' and createdTime < '{search_end}' and trashed=false",
+                    'fields': 'files(id,name,createdTime,size,mimeType)',
+                    'orderBy': 'createdTime desc',
+                    'pageSize': 50  # Increased to get more files
+                }
+            )
+            
+            meeting_files = []
+            event_title_clean = self._clean_title_for_matching(event_title)
+            
+            for file in files.get('files', []):
+                file_name_clean = self._clean_title_for_matching(file['name'])
+                mime_type = file.get('mimeType', '')
+                
+                # Check if file name contains meeting title or vice versa
+                if (event_title_clean in file_name_clean or 
+                    file_name_clean in event_title_clean or
+                    self._titles_match(file['name'], event_title)):
+                    
+                    meeting_files.append(file['id'])
+                    
+                    # Log with file type indicator
+                    file_type = "📹 Video"
+                    if 'video/' in mime_type:
+                        file_type = "📹 Video"
+                    elif 'application/pdf' in mime_type:
+                        file_type = "📄 PDF"
+                    elif 'application/vnd.google-apps' in mime_type:
+                        file_type = "📊 Google Doc"
+                    elif 'text/' in mime_type:
+                        file_type = "📝 Text"
+                    elif 'image/' in mime_type:
+                        file_type = "🖼️ Image"
+                    else:
+                        file_type = "📁 File"
+                    
+                    _logger.info(f"{file_type} Found for meeting '{event_title}': {file['name']} ({mime_type})")
+            
+            _logger.info(f"📊 Total files found for meeting '{event_title}': {len(meeting_files)}")
+            return meeting_files
+            
+        except Exception as e:
+            _logger.error(f"Error finding recordings for meeting {event_title}: {str(e)}")
+            return []
+
+    def _clean_title_for_matching(self, title):
+        """Clean title for better matching"""
+        import re
+        
+        # Remove common suffixes and prefixes
+        title = title.lower()
+        title = re.sub(r' - recording', '', title)
+        title = re.sub(r' - grabacion', '', title)
+        title = re.sub(r'recording', '', title)
+        title = re.sub(r'grabacion', '', title)
+        
+        # Remove date/time patterns
+        title = re.sub(r'\d{4}/\d{2}/\d{2}', '', title)
+        title = re.sub(r'\d{2}:\d{2}', '', title)
+        title = re.sub(r'cst', '', title)
+        
+        # Clean up extra spaces
+        title = ' '.join(title.split())
+        
+        return title
+
+    def _titles_match(self, file_name, event_title):
+        """Check if file name matches calendar event title"""
+        # Remove common suffixes from file name
+        file_clean = file_name.lower()
+        file_clean = file_clean.replace(' - recording', '').replace(' - grabacion', '')
+        file_clean = file_clean.replace('recording', '').replace('grabacion', '')
+        
+        # Remove date/time patterns
+        import re
+        file_clean = re.sub(r'\d{4}/\d{2}/\d{2}', '', file_clean)
+        file_clean = re.sub(r'\d{2}:\d{2}', '', file_clean)
+        file_clean = re.sub(r'cst', '', file_clean)
+        
+        # Clean up extra spaces
+        file_clean = ' '.join(file_clean.split())
+        
+        # Check if event title is contained in cleaned file name
+        return event_title.lower() in file_clean or file_clean in event_title.lower()
+
+    def _get_parent_folder_info(self, parent_ids, drive_service):
+        """Get information about parent folders to understand context"""
+        if not parent_ids:
+            return {}
+        
+        try:
+            parent_info = drive_service._do_request(
+                f'/drive/v3/files/{parent_ids[0]}',
+                params={
+                    'fields': 'id,name,parents'
+                }
+            )
+            return parent_info
+        except:
+            return {}
+
+    def _is_google_meet_event(self, event):
+        """Check if event is a Google Meet event"""
+        # Check for conference data
+        conference_data = event.get('conferenceData', {})
+        if conference_data.get('conferenceId'):
+            return True
+        
+        # Check for hangout link
+        if event.get('hangoutLink'):
+            return True
+        
+        # Check if attendees have Google Meet links
+        attendees = event.get('attendees', [])
+        for attendee in attendees:
+            if 'hangout.google.com' in str(attendee):
+                return True
+        
+        return False
+
+    def _extract_participants(self, event):
+        """Extract participant emails from event"""
+        participants = []
+        attendees = event.get('attendees', [])
+        
+        for attendee in attendees:
+            email = attendee.get('email')
+            if email and not email.endswith('@resource.calendar.google.com'):
+                participants.append(email)
+        
+        return participants
+
+    def _get_meeting_recordings(self, event):
+        """Get recording files for a meeting"""
+        # This is a simplified implementation
+        # In a real scenario, you would need to:
+        # 1. Get the meeting ID from the event
+        # 2. Call Google Drive API to search for recording files
+        # 3. Filter files that belong to this specific meeting
+        
+        try:
+            # For now, we'll search for any recording files in the user's Drive
+            # that might be related to meetings
+            drive_service = self.env['google.drive.service']
+            
+            # Search for video files created in the last few days
+            from datetime import datetime, timedelta
+            cutoff_date = (datetime.now() - timedelta(days=2)).isoformat()
+            
+            files = drive_service._do_request(
+                '/drive/v3/files',
+                params={
+                    'q': f"mimeType contains 'video/' and createdTime > '{cutoff_date}' and trashed=false",
+                    'fields': 'files(id,name,createdTime,parents)',
+                    'orderBy': 'createdTime desc',
+                    'pageSize': 50
+                }
+            )
+            
+            recording_files = []
+            for file in files.get('files', []):
+                # Simple heuristic: if file name contains meeting-related keywords
+                file_name = file['name'].lower()
+                if any(keyword in file_name for keyword in ['meet', 'meeting', 'reunion', 'grabacion', 'recording']):
+                    recording_files.append(file['id'])
+            
+            return recording_files
+            
+        except Exception as e:
+            _logger.error(f"Failed to get meeting recordings: {str(e)}")
+            return []
+
+    def get_calendar_list(self):
+        """Get list of available calendars"""
+        try:
+            calendars = self._do_request(
+                '/users/me/calendarList'
+            )
+            
+            return calendars.get('items', [])
+            
+        except Exception as e:
+            _logger.error(f"Failed to get calendar list: {str(e)}")
+            raise UserError(_('Failed to get calendar list: %s') % str(e))

+ 713 - 0
google_api/models/google_drive_service.py

@@ -0,0 +1,713 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+import json
+from datetime import datetime
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+TIMEOUT = 30
+GOOGLE_API_BASE_URL = 'https://www.googleapis.com'
+
+
+class GoogleDriveService(models.AbstractModel):
+    """Servicio para Google Drive API siguiendo las mejores prácticas de Odoo"""
+    _name = 'google.drive.service'
+    _description = 'Google Drive Service'
+
+    def _get_credentials(self, user=None):
+        """Obtener credenciales de configuración para el usuario especificado"""
+        if not user:
+            user = self.env.user
+        
+        config = self.env['ir.config_parameter'].sudo()
+        
+        enabled = config.get_param('google_api.enabled', 'False')
+        if not enabled or enabled == 'False':
+            raise UserError(_('Google API Integration is not enabled'))
+        
+        client_id = config.get_param('google_api.client_id', '')
+        client_secret = config.get_param('google_api.client_secret', '')
+        
+        if not client_id or not client_secret:
+            raise UserError(_('Google API credentials not configured'))
+        
+        # Get access token from user settings
+        user_settings = user.res_users_settings_id
+        if not user_settings or not user_settings._google_authenticated():
+            raise UserError(_('User %s is not authenticated with Google. Please connect your account first.') % user.name)
+        
+        access_token = user_settings._get_google_access_token()
+        if not access_token:
+            raise UserError(_('Could not obtain valid access token for user %s. Please reconnect your Google account.') % user.name)
+        
+        return {
+            'client_id': client_id,
+            'client_secret': client_secret,
+            'access_token': access_token
+        }
+
+    def _refresh_access_token(self):
+        """Refrescar el token de acceso"""
+        try:
+            config = self.env['ir.config_parameter'].sudo()
+            refresh_token = config.get_param('google_api.refresh_token')
+            client_id = config.get_param('google_api.client_id')
+            client_secret = config.get_param('google_api.client_secret')
+            
+            if not all([refresh_token, client_id, client_secret]):
+                return False
+            
+            token_url = 'https://oauth2.googleapis.com/token'
+            data = {
+                'client_id': client_id,
+                'client_secret': client_secret,
+                'refresh_token': refresh_token,
+                'grant_type': 'refresh_token'
+            }
+            
+            import requests
+            response = requests.post(token_url, data=data, timeout=TIMEOUT)
+            
+            if response.status_code == 200:
+                token_data = response.json()
+                new_access_token = token_data.get('access_token')
+                
+                if new_access_token:
+                    config.set_param('google_api.access_token', new_access_token)
+                    _logger.info("Google API access token refreshed successfully")
+                    return True
+            
+            return False
+            
+        except Exception as e:
+            _logger.error(f"Failed to refresh access token: {str(e)}")
+            return False
+
+    def _do_request(self, uri, params=None, headers=None, method='GET', timeout=TIMEOUT, json_data=None):
+        """Realizar petición HTTP a Google API con manejo de errores y reintentos"""
+        try:
+            credentials = self._get_credentials()
+            
+            # Headers por defecto
+            default_headers = {
+                'Authorization': f'Bearer {credentials["access_token"]}',
+                'Content-Type': 'application/json'
+            }
+            
+            if headers:
+                default_headers.update(headers)
+            
+            url = f"{GOOGLE_API_BASE_URL}{uri}"
+            
+            import requests
+            
+            # Realizar petición
+            if method.upper() == 'GET':
+                response = requests.get(url, params=params, headers=default_headers, timeout=timeout)
+            elif method.upper() == 'POST':
+                response = requests.post(url, params=params, json=json_data or params, headers=default_headers, timeout=timeout)
+            elif method.upper() == 'PUT':
+                response = requests.put(url, params=params, json=json_data or params, headers=default_headers, timeout=timeout)
+            elif method.upper() == 'PATCH':
+                response = requests.patch(url, params=params, json=json_data or params, headers=default_headers, timeout=timeout)
+            elif method.upper() == 'DELETE':
+                response = requests.delete(url, params=params, headers=default_headers, timeout=timeout)
+            else:
+                raise UserError(_('Unsupported HTTP method: %s') % method)
+            
+            # Manejar códigos de respuesta
+            if response.status_code == 200:
+                return response.json() if response.content else {}
+            elif response.status_code == 201:
+                return response.json() if response.content else {}
+            elif response.status_code == 204:
+                return {}
+            elif response.status_code == 401:
+                # Intentar refrescar token
+                if self._refresh_access_token():
+                    # Reintentar la petición
+                    return self._do_request(uri, params, headers, method, timeout)
+                else:
+                    raise UserError(_('Authentication failed. Please reconnect your Google account.'))
+            elif response.status_code == 429:
+                raise UserError(_('Rate limit exceeded. Please try again later.'))
+            elif response.status_code >= 400:
+                error_msg = self._parse_error_response(response)
+                raise UserError(error_msg)
+            
+            return response.json() if response.content else {}
+            
+        except requests.exceptions.Timeout:
+            raise UserError(_('Request timeout. Please try again.'))
+        except requests.exceptions.ConnectionError:
+            raise UserError(_('Connection error. Please check your internet connection.'))
+        except UserError:
+            raise
+        except Exception as e:
+            raise UserError(_('Unexpected error: %s') % str(e))
+
+    def _parse_error_response(self, response):
+        """Parsear respuesta de error de Google API"""
+        try:
+            error_data = response.json()
+            error_info = error_data.get('error', {})
+            
+            message = error_info.get('message', 'Unknown error')
+            code = error_info.get('code', response.status_code)
+            
+            return f"Google API Error {code}: {message}"
+        except:
+            return f"Google API Error {response.status_code}: {response.text}"
+
+    def _build_shared_drive_params(self, include_shared_drives=True):
+        """Construir parámetros para Shared Drives"""
+        params = {}
+        if include_shared_drives:
+            params.update({
+                'supportsAllDrives': 'true',
+                'includeItemsFromAllDrives': 'true'
+            })
+        return params
+
+    def validate_folder_id(self, folder_id):
+        """Validar que un folder ID existe y es accesible"""
+        try:
+            if not folder_id:
+                raise UserError(_('Folder ID is required'))
+            
+            if not isinstance(folder_id, str) or len(folder_id) < 10:
+                raise UserError(_('Invalid folder ID format'))
+            
+            # Paso 1: Intentar como carpeta regular
+            try:
+                response = self._do_request(f'/drive/v3/files/{folder_id}', {
+                    'fields': 'id,name,mimeType'
+                })
+                return {
+                    'valid': True,
+                    'type': 'regular_folder',
+                    'name': response.get('name', 'Unknown'),
+                    'id': response.get('id')
+                }
+            except UserError as e:
+                if '404' in str(e):
+                    # Paso 2: Intentar como Shared Drive
+                    try:
+                        response = self._do_request(f'/drive/v3/drives/{folder_id}', {
+                            'fields': 'id,name'
+                        })
+                        return {
+                            'valid': True,
+                            'type': 'shared_drive',
+                            'name': response.get('name', 'Unknown'),
+                            'id': response.get('id')
+                        }
+                    except UserError:
+                        # Paso 3: Intentar como carpeta dentro de Shared Drive
+                        try:
+                            params = self._build_shared_drive_params()
+                            params['fields'] = 'id,name,mimeType'
+                            response = self._do_request(f'/drive/v3/files/{folder_id}', params)
+                            return {
+                                'valid': True,
+                                'type': 'folder_in_shared_drive',
+                                'name': response.get('name', 'Unknown'),
+                                'id': response.get('id')
+                            }
+                        except UserError:
+                            raise UserError(_('Folder ID not found or not accessible'))
+                else:
+                    raise e
+                    
+        except Exception as e:
+            return {
+                'valid': False,
+                'error': str(e)
+            }
+
+    def create_folder(self, name, parent_folder_id=None, description=None):
+        """Crear una carpeta en Google Drive"""
+        try:
+            folder_metadata = {
+                'name': name,
+                'mimeType': 'application/vnd.google-apps.folder'
+            }
+            
+            if parent_folder_id:
+                folder_metadata['parents'] = [parent_folder_id]
+            
+            if description:
+                folder_metadata['description'] = description
+            
+            # Usar parámetros de Shared Drive si hay parent folder
+            params = {}
+            if parent_folder_id:
+                params = self._build_shared_drive_params()
+            
+            # Construir URL con parámetros
+            url = '/drive/v3/files'
+            if params:
+                param_string = '&'.join([f"{k}={v}" for k, v in params.items()])
+                url = f"{url}?{param_string}"
+            
+            response = self._do_request(url, folder_metadata, method='POST')
+            
+            return {
+                'success': True,
+                'folder_id': response.get('id'),
+                'folder_name': response.get('name'),
+                'folder_url': f"https://drive.google.com/drive/folders/{response.get('id')}"
+            }
+            
+        except Exception as e:
+            return {
+                'success': False,
+                'error': str(e)
+            }
+
+    def list_folders(self, parent_folder_id=None, include_shared_drives=True):
+        """Listar carpetas en Google Drive"""
+        try:
+            query = "mimeType='application/vnd.google-apps.folder'"
+            if parent_folder_id:
+                query += f" and '{parent_folder_id}' in parents"
+            
+            params = {
+                'q': query,
+                'fields': 'files(id,name,parents,createdTime,modifiedTime)',
+                'orderBy': 'name'
+            }
+            
+            # Agregar parámetros de Shared Drive
+            if include_shared_drives:
+                params.update(self._build_shared_drive_params())
+            
+            response = self._do_request('/drive/v3/files', params)
+            
+            return {
+                'success': True,
+                'folders': response.get('files', [])
+            }
+            
+        except Exception as e:
+            return {
+                'success': False,
+                'error': str(e)
+            }
+
+    def get_folder_url(self, folder_id):
+        """Obtener URL para abrir una carpeta"""
+        try:
+            if not folder_id:
+                raise UserError(_('Folder ID is required'))
+            
+            # Validar que existe
+            validation = self.validate_folder_id(folder_id)
+            if not validation.get('valid'):
+                raise UserError(validation.get('error', 'Invalid folder'))
+            
+            folder_type = validation.get('type', 'regular_folder')
+            
+            if folder_type == 'shared_drive':
+                return f"https://drive.google.com/drive/u/0/folders/{folder_id}"
+            else:
+                return f"https://drive.google.com/drive/folders/{folder_id}"
+                
+        except Exception as e:
+            return None
+
+    def test_connection(self):
+        """Probar conexión al servicio"""
+        try:
+            credentials = self._get_credentials()
+            
+            # Test básico de autenticación
+            import requests
+            response = requests.get(
+                f"{GOOGLE_API_BASE_URL}/oauth2/v1/userinfo",
+                headers={'Authorization': f'Bearer {credentials["access_token"]}'},
+                timeout=10
+            )
+            
+            if response.status_code == 200:
+                user_info = response.json()
+                return {
+                    'success': True,
+                    'user_email': user_info.get('email', 'Unknown'),
+                    'user_name': user_info.get('name', 'Unknown')
+                }
+            else:
+                return {
+                    'success': False,
+                    'error': f"Authentication failed: {response.status_code}"
+                }
+                
+        except Exception as e:
+            return {
+                'success': False,
+                'error': str(e)
+            }
+
+    # ============================================================================
+    # MÉTODOS DE NAVEGACIÓN Y ESTRUCTURA
+    # ============================================================================
+
+    def navigate_folder_hierarchy(self, folder_id, max_levels=5):
+        """Navigate up the folder hierarchy and return the complete path"""
+        try:
+            hierarchy = []
+            current_id = folder_id
+            
+            for level in range(max_levels):
+                try:
+                    response = self._do_request(f'/drive/v3/files/{current_id}', {
+                        'fields': 'id,name,parents'
+                    })
+                    
+                    folder_info = {
+                        'id': response.get('id'),
+                        'name': response.get('name', ''),
+                        'level': level
+                    }
+                    
+                    hierarchy.append(folder_info)
+                    
+                    parent_ids = response.get('parents', [])
+                    if not parent_ids:
+                        break
+                    
+                    current_id = parent_ids[0]
+                    
+                except Exception as e:
+                    _logger.error(f"Error navigating folder hierarchy at level {level}: {str(e)}")
+                    break
+            
+            return hierarchy
+            
+        except Exception as e:
+            _logger.error(f"Error in navigate_folder_hierarchy: {str(e)}")
+            return []
+
+    def find_folders_by_name(self, parent_id, name_pattern):
+        """Find folders by name pattern in a parent folder"""
+        try:
+            import re
+            
+            query = f"'{parent_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false"
+            
+            params = {
+                'q': query,
+                'fields': 'files(id,name)',
+                'pageSize': 100
+            }
+            
+            # Add Shared Drive parameters
+            params.update(self._build_shared_drive_params())
+            
+            response = self._do_request('/drive/v3/files', params)
+            folders = response.get('files', [])
+            
+            # Filter by name pattern
+            pattern = re.compile(name_pattern)
+            matching_folders = [folder for folder in folders if pattern.match(folder.get('name', ''))]
+            
+            return matching_folders
+            
+        except Exception as e:
+            _logger.error(f"Error finding folders by name: {str(e)}")
+            return []
+
+    def get_folder_info(self, folder_id):
+        """Get detailed information about a folder"""
+        try:
+            validation = self.validate_folder_id(folder_id)
+            if not validation.get('valid'):
+                return None
+            
+            response = self._do_request(f'/drive/v3/files/{folder_id}', {
+                'fields': 'id,name,parents,createdTime,modifiedTime,mimeType'
+            })
+            
+            return response
+            
+        except Exception as e:
+            _logger.error(f"Error getting folder info: {str(e)}")
+            return None
+
+    # ============================================================================
+    # MÉTODOS DE MANIPULACIÓN DE CARPETAS
+    # ============================================================================
+
+    def rename_folder(self, folder_id, new_name):
+        """Rename a folder in Google Drive"""
+        try:
+            # For Google Drive API v3, we need to use PATCH with the correct parameters
+            # The issue was that we were passing params instead of json body
+            update_data = {
+                'name': new_name
+            }
+            
+            # Use PATCH method with json body (not params)
+            response = self._do_request(f'/drive/v3/files/{folder_id}', 
+                                      json_data=update_data, 
+                                      method='PATCH')
+            
+            return {
+                'success': True,
+                'folder_id': response.get('id'),
+                'new_name': response.get('name')
+            }
+            
+        except Exception as e:
+            return {
+                'success': False,
+                'error': str(e)
+            }
+
+    def move_folder(self, folder_id, new_parent_id):
+        """Move a folder to a new parent"""
+        try:
+            # Get current folder info
+            response = self._do_request(f'/drive/v3/files/{folder_id}', {
+                'fields': 'parents'
+            })
+            
+            current_parents = response.get('parents', [])
+            
+            if not current_parents:
+                return {
+                    'success': False,
+                    'error': 'Folder has no current parent'
+                }
+            
+            # Use PATCH to update the parents
+            update_data = {
+                'addParents': new_parent_id,
+                'removeParents': current_parents[0]
+            }
+            
+            # Update the file with new parent
+            update_response = self._do_request(
+                f'/drive/v3/files/{folder_id}',
+                update_data,
+                method='PATCH',
+                json_data=update_data
+            )
+            
+            return {
+                'success': True,
+                'folder_id': folder_id,
+                'new_parent_id': new_parent_id
+            }
+            
+        except Exception as e:
+            return {
+                'success': False,
+                'error': str(e)
+            }
+
+    def delete_folder(self, folder_id):
+        """Delete a folder from Google Drive"""
+        try:
+            self._do_request(f'/drive/v3/files/{folder_id}', method='DELETE')
+            
+            return {
+                'success': True,
+                'folder_id': folder_id
+            }
+            
+        except Exception as e:
+            return {
+                'success': False,
+                'error': str(e)
+            }
+
+    # ============================================================================
+    # MÉTODOS DE VALIDACIÓN Y UTILIDADES
+    # ============================================================================
+
+    def extract_folder_id_from_url(self, url):
+        """Extract folder ID from Google Drive URL - Generic utility method"""
+        if not url:
+            return None
+        
+        # Handle different Google Drive URL formats
+        import re
+        
+        # Format: https://drive.google.com/drive/folders/FOLDER_ID
+        folder_pattern = r'drive\.google\.com/drive/folders/([a-zA-Z0-9_-]+)'
+        match = re.search(folder_pattern, url)
+        
+        if match:
+            return match.group(1)
+        
+        # Format: https://drive.google.com/open?id=FOLDER_ID
+        open_pattern = r'drive\.google\.com/open\?id=([a-zA-Z0-9_-]+)'
+        match = re.search(open_pattern, url)
+        
+        if match:
+            return match.group(1)
+        
+        # Check if it's already a folder ID
+        if isinstance(url, str) and len(url) >= 10 and len(url) <= 50:
+            if re.match(r'^[a-zA-Z0-9_-]+$', url):
+                return url
+        
+        return None
+
+    def sanitize_folder_name(self, name):
+        """Sanitize folder name to be Google Drive compatible - Generic utility method"""
+        if not name:
+            return 'Sin nombre'
+        
+        import re
+        
+        # Use regex for better performance
+        sanitized_name = re.sub(r'[<>:"|?*/\\]', '_', name)
+        
+        # Remove leading/trailing spaces and dots
+        sanitized_name = sanitized_name.strip(' .')
+        
+        # Ensure it's not empty after sanitization
+        if not sanitized_name:
+            return 'Sin nombre'
+        
+        # Limit length to 255 characters (Google Drive limit)
+        if len(sanitized_name) > 255:
+            sanitized_name = sanitized_name[:252] + '...'
+        
+        return sanitized_name
+
+    def create_or_get_folder(self, parent_folder_id, folder_name, description=None):
+        """Create a folder or get existing one by name - Generic utility method"""
+        try:
+            # First, check if folder already exists
+            existing_folders = self.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
+            if existing_folders:
+                return existing_folders[0]['id']
+            
+            # Create new folder
+            result = self.create_folder(folder_name, parent_folder_id, description)
+            
+            if result.get('success'):
+                return result.get('folder_id')
+            else:
+                error_msg = result.get('error', 'Unknown error')
+                raise UserError(_('Failed to create Google Drive folder "%s": %s') % (folder_name, error_msg))
+                
+        except Exception as e:
+            _logger.error(f"Error creating or getting folder {folder_name}: {str(e)}")
+            raise
+
+    def create_folder_structure(self, root_folder_id, structure_components):
+        """Create folder structure from components - Generic utility method"""
+        try:
+            created_folders = {}
+            
+            for level, (folder_name, parent_id) in enumerate(structure_components):
+                if level == 0:
+                    # First level uses root_folder_id as parent
+                    parent_folder_id = root_folder_id
+                else:
+                    # Other levels use the previous folder as parent
+                    parent_folder_id = created_folders[level - 1]
+                
+                folder_id = self.create_or_get_folder(parent_folder_id, folder_name)
+                created_folders[level] = folder_id
+            
+            return created_folders
+            
+        except Exception as e:
+            _logger.error(f"Error creating folder structure: {str(e)}")
+            raise
+
+    def find_existing_folder_structure(self, root_folder_id, structure_components):
+        """Find existing folder structure - Generic utility method"""
+        try:
+            found_folders = {}
+            
+            for level, (folder_name, parent_id) in enumerate(structure_components):
+                if level == 0:
+                    # First level uses root_folder_id as parent
+                    parent_folder_id = root_folder_id
+                else:
+                    # Other levels use the previous folder as parent
+                    parent_folder_id = found_folders[level - 1]
+                
+                folders = self.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
+                if not folders:
+                    return None  # Structure not found
+                
+                found_folders[level] = folders[0]['id']
+            
+            return found_folders
+            
+        except Exception as e:
+            _logger.error(f"Error finding existing folder structure: {str(e)}")
+            return None
+
+    def validate_folder_id_with_google_drive(self, folder_id):
+        """Validate if the folder ID exists and is accessible in Google Drive - Generic utility method"""
+        try:
+            validation = self.validate_folder_id(folder_id)
+            
+            if validation.get('valid'):
+                folder_name = validation.get('name', 'Unknown')
+                _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
+                return True, folder_name
+            else:
+                error_message = validation.get('error', 'Unknown error')
+                _logger.warning(f"❌ Folder ID {folder_id} validation failed: {error_message}")
+                return False, error_message
+                
+        except Exception as e:
+            _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
+            return False, str(e)
+
+    def check_folder_belongs_to_parent(self, folder_id, expected_parent_id, max_levels=5):
+        """Check if a folder belongs to a specific parent in the hierarchy"""
+        try:
+            if not expected_parent_id:
+                return False
+            
+            # Get current folder info
+            response = self._do_request(f'/drive/v3/files/{folder_id}', {
+                'fields': 'parents'
+            })
+            
+            parent_ids = response.get('parents', [])
+            
+            if not parent_ids:
+                return False
+            
+            # Navigate up to find if the folder is under the expected parent
+            current_parent_id = parent_ids[0]
+            
+            for _ in range(max_levels):
+                # Check if current parent is the expected parent
+                if current_parent_id == expected_parent_id:
+                    # Folder is under the expected parent
+                    return True
+                
+                parent_response = self._do_request(f'/drive/v3/files/{current_parent_id}', {
+                    'fields': 'parents'
+                })
+                
+                parent_parent_ids = parent_response.get('parents', [])
+                
+                if not parent_parent_ids:
+                    # Reached the top level, folder is not under the expected parent
+                    return False
+                
+                current_parent_id = parent_parent_ids[0]
+            
+            # If we reach here, folder is not under the expected parent
+            return False
+            
+        except Exception as e:
+            _logger.error(f"Error checking folder parent relationship: {str(e)}")
+            return False

+ 252 - 0
google_api/models/google_workspace_service.py

@@ -0,0 +1,252 @@
+# -*- coding: utf-8 -*-
+
+import logging
+from odoo import models, api
+from odoo.exceptions import UserError
+from odoo.tools.translate import _
+
+_logger = logging.getLogger(__name__)
+
+
+class GoogleWorkspaceService(models.AbstractModel):
+    _name = 'google.workspace.service'
+    _description = 'Google Workspace Service'
+
+    def list_shared_drives(self):
+        """List all accessible Shared Drives"""
+        try:
+            drive_service = self.env['google.drive.service']
+            
+            # Use the drive service to make the request
+            response = drive_service._do_request('/drive/v3/drives', {
+                'pageSize': 100,
+                'fields': 'drives(id,name,createdTime,capabilities)'
+            })
+            
+            shared_drives = response.get('drives', [])
+            
+            return {
+                'success': True,
+                'shared_drives': shared_drives
+            }
+            
+        except Exception as e:
+            _logger.error(f"Error listing shared drives: {str(e)}")
+            return {
+                'success': False,
+                'error': str(e)
+            }
+
+    def get_shared_drive_info(self, drive_id):
+        """Get information about a specific Shared Drive"""
+        try:
+            drive_service = self.env['google.drive.service']
+            
+            response = drive_service._do_request(f'/drive/v3/drives/{drive_id}', {
+                'fields': 'id,name,createdTime,capabilities,restrictions'
+            })
+            
+            return {
+                'success': True,
+                'drive_info': response
+            }
+            
+        except Exception as e:
+            _logger.error(f"Error getting shared drive info: {str(e)}")
+            return {
+                'success': False,
+                'error': str(e)
+            }
+
+    def list_files_in_shared_drive(self, drive_id, folder_id=None):
+        """List files in a Shared Drive or specific folder within it"""
+        try:
+            drive_service = self.env['google.drive.service']
+            
+            # Build query
+            query = f"'{drive_id}' in parents and trashed=false"
+            if folder_id:
+                query = f"'{folder_id}' in parents and trashed=false"
+            
+            params = {
+                'q': query,
+                'fields': 'files(id,name,mimeType,createdTime,modifiedTime,parents)',
+                'pageSize': 100,
+                'orderBy': 'name'
+            }
+            
+            # Add Shared Drive parameters
+            params.update(drive_service._build_shared_drive_params())
+            
+            response = drive_service._do_request('/drive/v3/files', params)
+            
+            return {
+                'success': True,
+                'files': response.get('files', [])
+            }
+            
+        except Exception as e:
+            _logger.error(f"Error listing files in shared drive: {str(e)}")
+            return {
+                'success': False,
+                'error': str(e)
+            }
+
+    def check_shared_drive_permissions(self, drive_id):
+        """Check current user's permissions on a Shared Drive"""
+        try:
+            drive_service = self.env['google.drive.service']
+            
+            # Get drive info with capabilities
+            response = drive_service._do_request(f'/drive/v3/drives/{drive_id}', {
+                'fields': 'capabilities'
+            })
+            
+            capabilities = response.get('capabilities', {})
+            
+            permissions = {
+                'can_add_children': capabilities.get('canAddChildren', False),
+                'can_comment': capabilities.get('canComment', False),
+                'can_copy': capabilities.get('canCopy', False),
+                'can_delete': capabilities.get('canDelete', False),
+                'can_download': capabilities.get('canDownload', False),
+                'can_edit': capabilities.get('canEdit', False),
+                'can_list_children': capabilities.get('canListChildren', False),
+                'can_move_item_into_team_drive': capabilities.get('canMoveItemIntoTeamDrive', False),
+                'can_move_item_out_of_team_drive': capabilities.get('canMoveItemOutOfTeamDrive', False),
+                'can_move_item_within_team_drive': capabilities.get('canMoveItemWithinTeamDrive', False),
+                'can_read': capabilities.get('canRead', False),
+                'can_read_revisions': capabilities.get('canReadRevisions', False),
+                'can_remove_children': capabilities.get('canRemoveChildren', False),
+                'can_rename': capabilities.get('canRename', False),
+                'can_share': capabilities.get('canShare', False),
+                'can_trash': capabilities.get('canTrash', False),
+                'can_trash_children': capabilities.get('canTrashChildren', False),
+            }
+            
+            return {
+                'success': True,
+                'permissions': permissions
+            }
+            
+        except Exception as e:
+            _logger.error(f"Error checking shared drive permissions: {str(e)}")
+            return {
+                'success': False,
+                'error': str(e)
+            }
+
+    def create_folder_in_shared_drive(self, name, drive_id, parent_folder_id=None, description=None):
+        """Create a folder within a Shared Drive"""
+        try:
+            drive_service = self.env['google.drive.service']
+            
+            folder_metadata = {
+                'name': name,
+                'mimeType': 'application/vnd.google-apps.folder'
+            }
+            
+            if parent_folder_id:
+                folder_metadata['parents'] = [parent_folder_id]
+            
+            if description:
+                folder_metadata['description'] = description
+            
+            # Add Shared Drive parameters
+            params = drive_service._build_shared_drive_params()
+            
+            # Build URL with parameters
+            url = '/drive/v3/files'
+            if params:
+                param_string = '&'.join([f"{k}={v}" for k, v in params.items()])
+                url = f"{url}?{param_string}"
+            
+            response = drive_service._do_request(url, folder_metadata, method='POST')
+            
+            return {
+                'success': True,
+                'folder_id': response.get('id'),
+                'folder_name': response.get('name'),
+                'folder_url': f"https://drive.google.com/drive/folders/{response.get('id')}"
+            }
+            
+        except Exception as e:
+            _logger.error(f"Error creating folder in shared drive: {str(e)}")
+            return {
+                'success': False,
+                'error': str(e)
+            }
+
+    def validate_shared_drive_access(self, drive_id):
+        """Validate that the current user has access to a Shared Drive"""
+        try:
+            # Try to get drive info
+            drive_info = self.get_shared_drive_info(drive_id)
+            
+            if not drive_info.get('success'):
+                return {
+                    'valid': False,
+                    'error': drive_info.get('error', 'Unknown error')
+                }
+            
+            # Check permissions
+            permissions = self.check_shared_drive_permissions(drive_id)
+            
+            if not permissions.get('success'):
+                return {
+                    'valid': False,
+                    'error': permissions.get('error', 'Cannot check permissions')
+                }
+            
+            # Check if user has at least read access
+            user_permissions = permissions.get('permissions', {})
+            if not user_permissions.get('can_read', False):
+                return {
+                    'valid': False,
+                    'error': 'No read access to this Shared Drive'
+                }
+            
+            return {
+                'valid': True,
+                'drive_name': drive_info.get('drive_info', {}).get('name', 'Unknown'),
+                'permissions': user_permissions
+            }
+            
+        except Exception as e:
+            _logger.error(f"Error validating shared drive access: {str(e)}")
+            return {
+                'valid': False,
+                'error': str(e)
+            }
+
+    def search_shared_drives_by_name(self, name_pattern):
+        """Search for Shared Drives by name pattern"""
+        try:
+            import re
+            
+            # Get all shared drives
+            shared_drives_result = self.list_shared_drives()
+            
+            if not shared_drives_result.get('success'):
+                return {
+                    'success': False,
+                    'error': shared_drives_result.get('error', 'Cannot list shared drives')
+                }
+            
+            shared_drives = shared_drives_result.get('shared_drives', [])
+            
+            # Filter by name pattern
+            pattern = re.compile(name_pattern, re.IGNORECASE)
+            matching_drives = [drive for drive in shared_drives if pattern.search(drive.get('name', ''))]
+            
+            return {
+                'success': True,
+                'matching_drives': matching_drives
+            }
+            
+        except Exception as e:
+            _logger.error(f"Error searching shared drives: {str(e)}")
+            return {
+                'success': False,
+                'error': str(e)
+            }

+ 16 - 140
google_api/models/res_config_settings.py

@@ -3,7 +3,7 @@
 
 import requests
 import json
-from odoo import fields, models, _
+from odoo import fields, models, api, _
 from odoo.exceptions import UserError
 
 
@@ -124,44 +124,6 @@ class ResConfigSettings(models.TransientModel):
             }
         }
 
-    def action_connect_google_account(self):
-        """Connect Google account using manual OAuth flow"""
-        self.ensure_one()
-        
-        if not self.google_api_client_id or not self.google_api_client_secret:
-            raise UserError(_('Please configure Google API credentials first'))
-        
-        # Get base URL
-        base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
-        
-        # Create a custom redirect URI for our manual flow
-        redirect_uri = f"{base_url}/web/google_oauth_callback"
-        
-        # Build OAuth authorization URL
-        auth_url = "https://accounts.google.com/o/oauth2/auth"
-        params = {
-            'client_id': self.google_api_client_id,
-            'redirect_uri': redirect_uri,
-            'scope': 'https://www.googleapis.com/auth/drive',
-            'response_type': 'code',
-            'access_type': 'offline',
-            'prompt': 'consent',
-            'state': 'google_drive_integration'
-        }
-        
-        # Build URL with parameters
-        param_string = '&'.join([f"{k}={v}" for k, v in params.items()])
-        oauth_url = f"{auth_url}?{param_string}"
-        
-        # Store the redirect URI for later use
-        self.env['ir.config_parameter'].sudo().set_param('google_api.manual_redirect_uri', redirect_uri)
-        
-        return {
-            'type': 'ir.actions.act_url',
-            'url': oauth_url,
-            'target': 'new',
-        }
-
     def action_show_oauth_redirect_uris(self):
         """Show the redirect URIs that need to be configured in Google Cloud Console"""
         self.ensure_one()
@@ -180,104 +142,18 @@ class ResConfigSettings(models.TransientModel):
             }
         }
 
-    def action_test_google_oauth_connection(self):
-        """Test OAuth connection using manual OAuth flow"""
-        self.ensure_one()
-        
-        # Check for manual OAuth token
-        access_token = self.env['ir.config_parameter'].sudo().get_param('google_api.access_token')
-        
-        if not access_token:
-            return {
-                'type': 'ir.actions.client',
-                'tag': 'display_notification',
-                'params': {
-                    'title': _('Info'),
-                    'message': _('No OAuth token found. Please use "Connect Google Account" to connect your Google account.'),
-                    'type': 'info',
-                    'sticky': False,
-                }
-            }
-        
-        try:
-            # Test Google Drive API access
-            headers = {
-                'Authorization': f'Bearer {access_token}',
-            }
-            
-            # Try to get user info (this will test the token)
-            response = requests.get(
-                'https://www.googleapis.com/drive/v3/about?fields=user',
-                headers=headers,
-                timeout=10
-            )
-            
-            if response.status_code == 200:
-                user_info = response.json()
-                user_email = user_info.get('user', {}).get('emailAddress', 'Unknown')
-                
-                return {
-                    'type': 'ir.actions.client',
-                    'tag': 'display_notification',
-                    'params': {
-                        'title': _('Success'),
-                        'message': _('OAuth connection successful! Connected as: %s') % user_email,
-                        'type': 'success',
-                        'sticky': False,
-                    }
-                }
-            elif response.status_code == 401:
-                # Try to refresh the token
-                if self._refresh_access_token():
-                    return {
-                        'type': 'ir.actions.client',
-                        'tag': 'display_notification',
-                        'params': {
-                            'title': _('Token Refreshed'),
-                            'message': _('Access token was refreshed successfully. Please try the test again.'),
-                            'type': 'success',
-                        }
-                    }
-                else:
-                    raise UserError(_('OAuth token has expired and could not be refreshed. Please reconnect your Google account.'))
-            else:
-                raise UserError(_('Google Drive API test failed. Status: %s') % response.status_code)
-                
-        except Exception as e:
-            raise UserError(_('OAuth connection test failed: %s') % str(e))
-
-    def _refresh_access_token(self):
-        """Refresh the access token using the refresh token"""
-        try:
-            refresh_token = self.env['ir.config_parameter'].sudo().get_param('google_api.refresh_token')
-            client_id = self.env['ir.config_parameter'].sudo().get_param('google_api.client_id')
-            client_secret = self.env['ir.config_parameter'].sudo().get_param('google_api.client_secret')
-            
-            if not all([refresh_token, client_id, client_secret]):
-                return False
-            
-            # Exchange refresh token for new access token
-            token_url = 'https://oauth2.googleapis.com/token'
-            data = {
-                'client_id': client_id,
-                'client_secret': client_secret,
-                'refresh_token': refresh_token,
-                'grant_type': 'refresh_token'
-            }
-            
-            response = requests.post(token_url, data=data, timeout=30)
-            
-            if response.status_code == 200:
-                token_data = response.json()
-                new_access_token = token_data.get('access_token')
-                
-                if new_access_token:
-                    # Store the new access token
-                    self.env['ir.config_parameter'].sudo().set_param('google_api.access_token', new_access_token)
-                    return True
-            
-            return False
-            
-        except Exception as e:
-            _logger.error(f"Failed to refresh access token: {str(e)}")
-            return False
+    @api.model
+    def set_values(self):
+        super().set_values()
+        IrConfigParameter = self.env['ir.config_parameter'].sudo()
+        
+        # Set our custom parameters
+        IrConfigParameter.set_param('google_api.client_id', self.google_api_client_id or '')
+        IrConfigParameter.set_param('google_api.client_secret', self.google_api_client_secret or '')
+        IrConfigParameter.set_param('google_api.enabled', self.google_api_enabled)
+        
+        # Also set the parameters expected by google.service for compatibility
+        if self.google_api_client_id:
+            IrConfigParameter.set_param('google_drive_client_id', self.google_api_client_id)
+        if self.google_api_client_secret:
+            IrConfigParameter.set_param('google_drive_client_secret', self.google_api_client_secret)

+ 91 - 0
google_api/models/res_users.py

@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+
+
+class ResUsers(models.Model):
+    _inherit = "res.users"
+
+    # Computed fields for Google integration
+    google_connected = fields.Boolean(
+        string='Google Connected',
+        compute='_compute_google_status',
+        store=False,
+        help='Indicates if the user has connected their Google account'
+    )
+    
+    google_email = fields.Char(
+        string='Google Email',
+        compute='_compute_google_status',
+        store=False,
+        help='Email address associated with the connected Google account'
+    )
+    
+    # CRM Meets Files Configuration - Related field
+    google_crm_meets_folder_id = fields.Char(
+        string='Archivos Meets CRM',
+        related='res_users_settings_id.google_crm_meets_folder_id',
+        readonly=False,
+        help='ID de la carpeta en Google Drive donde se almacenan archivos de meets para sincronización con CRM'
+    )
+
+    @api.depends('res_users_settings_id.google_rtoken', 'res_users_settings_id.google_email')
+    def _compute_google_status(self):
+        """Compute Google connection status and email"""
+        for user in self:
+            settings = user.res_users_settings_id
+            if settings and settings._google_authenticated():
+                user.google_connected = True
+                user.google_email = settings.google_email or 'Connected'
+            else:
+                user.google_connected = False
+                user.google_email = False
+
+    def action_connect_google(self):
+        """Connect user's Google account"""
+        self.ensure_one()
+        
+        # Ensure user has settings record
+        if not self.res_users_settings_id:
+            self.env['res.users.settings'].create({'user_id': self.id})
+        
+        return self.res_users_settings_id.action_connect_google()
+
+    def action_disconnect_google(self):
+        """Disconnect user's Google account"""
+        self.ensure_one()
+        
+        if self.res_users_settings_id:
+            return self.res_users_settings_id.action_disconnect_google()
+        
+        return {
+            'type': 'ir.actions.client',
+            'tag': 'display_notification',
+            'params': {
+                'title': _('Info'),
+                'message': _('No Google account connected.'),
+                'type': 'info',
+                'sticky': False,
+            }
+        }
+
+    def action_test_google_connection(self):
+        """Test user's Google connection"""
+        self.ensure_one()
+        
+        if self.res_users_settings_id:
+            return self.res_users_settings_id.action_test_google_connection()
+        
+        return {
+            'type': 'ir.actions.client',
+            'tag': 'display_notification',
+            'params': {
+                'title': _('Info'),
+                'message': _('No Google account connected.'),
+                'type': 'info',
+                'sticky': False,
+            }
+        }
+
+

+ 254 - 0
google_api/models/res_users_settings.py

@@ -0,0 +1,254 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import timedelta
+import logging
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+
+class ResUsersSettings(models.Model):
+    _inherit = "res.users.settings"
+
+    # Google API tokens and synchronization information
+    google_rtoken = fields.Char('Google Refresh Token', copy=False, groups='base.group_system')
+    google_token = fields.Char('Google User Token', copy=False, groups='base.group_system')
+    google_token_validity = fields.Datetime('Google Token Validity', copy=False, groups='base.group_system')
+    google_email = fields.Char('Google Email', copy=False, groups='base.group_system')
+    google_sync_enabled = fields.Boolean('Google Sync Enabled', default=False, copy=False, groups='base.group_system')
+    
+    # CRM Meets Files Configuration
+    google_crm_meets_folder_id = fields.Char(
+        string='Archivos Meets CRM',
+        help='ID de la carpeta en Google Drive donde se almacenan archivos de meets para sincronización con CRM',
+        copy=False,
+        groups='base.group_system'
+    )
+
+    @api.model
+    def _get_fields_blacklist(self):
+        """Get list of google drive fields that won't be formatted in session_info."""
+        google_fields_blacklist = [
+            'google_rtoken',
+            'google_token',
+            'google_token_validity',
+            'google_email',
+            'google_sync_enabled',
+            'google_crm_meets_folder_id'
+        ]
+        return super()._get_fields_blacklist() + google_fields_blacklist
+
+    def _set_google_auth_tokens(self, access_token, refresh_token, expires_at):
+        """Set Google authentication tokens for the user"""
+        self.sudo().write({
+            'google_rtoken': refresh_token,
+            'google_token': access_token,
+            'google_token_validity': expires_at if expires_at else False,
+        })
+
+    def _google_authenticated(self):
+        """Check if user is authenticated with Google"""
+        self.ensure_one()
+        return bool(self.sudo().google_rtoken)
+
+    def _is_google_token_valid(self):
+        """Check if Google token is still valid"""
+        self.ensure_one()
+        return (self.sudo().google_token_validity and 
+                self.sudo().google_token_validity >= (fields.Datetime.now() + timedelta(minutes=1)))
+
+    def _refresh_google_token(self):
+        """Refresh Google access token using refresh token"""
+        self.ensure_one()
+
+        try:
+            access_token, ttl = self.env['google.service']._refresh_google_token('drive', self.sudo().google_rtoken)
+            self.sudo().write({
+                'google_token': access_token,
+                'google_token_validity': fields.Datetime.now() + timedelta(seconds=ttl),
+            })
+            _logger.info(f"Google token refreshed for user {self.user_id.name}")
+            return True
+        except Exception as e:
+            _logger.error(f"Failed to refresh Google token for user {self.user_id.name}: {str(e)}")
+            # Delete invalid tokens
+            self.env.cr.rollback()
+            self.sudo()._set_google_auth_tokens(False, False, False)
+            self.env.cr.commit()
+            return False
+
+    def _get_google_access_token(self):
+        """Get a valid Google access token, refreshing if necessary"""
+        self.ensure_one()
+        
+        if not self._google_authenticated():
+            return None
+        
+        if not self._is_google_token_valid():
+            if not self._refresh_google_token():
+                return None
+        
+        return self.sudo().google_token
+
+    def action_connect_google(self):
+        """Connect user's Google account"""
+        self.ensure_one()
+        
+        # Get Google API credentials from system settings
+        config = self.env['ir.config_parameter'].sudo()
+        client_id = config.get_param('google_api.client_id', '')
+        client_secret = config.get_param('google_api.client_secret', '')
+
+
+
+        if not client_id or not client_secret:
+            _logger.error("Google API credentials not configured.")
+            raise UserError(_('Google API credentials not configured. Please contact your administrator.'))
+        
+        # Build OAuth authorization URL
+        base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+        redirect_uri = f"{base_url}/web/google_oauth_callback"
+        
+        scope = 'https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/calendar'
+        
+        # Generate the authorization URL using our own credentials
+        authorize_uri = self._get_google_authorize_uri(
+            client_id=client_id,
+            redirect_uri=redirect_uri,
+            scope=scope,
+            state=str(self.id) # Pass user settings ID as state
+        )
+
+        
+        return {
+            'type': 'ir.actions.act_url',
+            'url': authorize_uri,
+            'target': 'new',
+        }
+
+    def action_disconnect_google(self):
+        """Disconnect user's Google account"""
+        self.ensure_one()
+        
+        self.sudo()._set_google_auth_tokens(False, False, False)
+        self.sudo().write({
+            'google_email': False,
+            'google_sync_enabled': False,
+        })
+        
+        return {
+            'type': 'ir.actions.client',
+            'tag': 'display_notification',
+            'params': {
+                'title': _('Success'),
+                'message': _('Google account disconnected successfully.'),
+                'type': 'success',
+                'sticky': False,
+            }
+        }
+
+    def action_test_google_connection(self):
+        """Test Google connection for the user"""
+        self.ensure_one()
+        
+        if not self._google_authenticated():
+            raise UserError(_('Google account not connected. Please connect your account first.'))
+        
+        access_token = self._get_google_access_token()
+        if not access_token:
+            raise UserError(_('Could not obtain valid access token. Please reconnect your Google account.'))
+        
+        try:
+            # Test Google Drive API access
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+            
+            response = self.env['google.drive.service']._do_request(
+                '/drive/v3/about',
+                params={'fields': 'user'},
+                headers=headers
+            )
+            
+            if response and 'user' in response:
+                user_email = response['user'].get('emailAddress', 'Unknown')
+                self.sudo().write({'google_email': user_email})
+                
+                return {
+                    'type': 'ir.actions.client',
+                    'tag': 'display_notification',
+                    'params': {
+                        'title': _('Success'),
+                        'message': _('Google connection successful! Connected as: %s') % user_email,
+                        'type': 'success',
+                        'sticky': False,
+                    }
+                }
+            else:
+                raise UserError(_('Could not retrieve user information from Google.'))
+                
+        except Exception as e:
+            _logger.error(f"Google connection test failed for user {self.user_id.name}: {str(e)}")
+            raise UserError(_('Google connection test failed: %s') % str(e))
+
+
+
+    def _get_google_authorize_uri(self, client_id, redirect_uri, scope, state):
+        """Generate Google OAuth authorization URI using our own credentials"""
+        import urllib.parse
+        
+        params = {
+            'response_type': 'code',
+            'client_id': client_id,
+            'redirect_uri': redirect_uri,
+            'scope': scope,
+            'state': state,
+            'access_type': 'offline',
+            'prompt': 'consent select_account',  # Force account selection
+            'hd': '',  # Allow any hosted domain
+            'include_granted_scopes': 'true'
+        }
+        
+        base_url = 'https://accounts.google.com/o/oauth2/auth'
+        query_string = urllib.parse.urlencode(params)
+        return f"{base_url}?{query_string}"
+
+    def _exchange_authorization_code_for_tokens(self, code, client_id, client_secret, redirect_uri):
+        """Exchange authorization code for access and refresh tokens"""
+        import requests
+        
+        token_url = 'https://oauth2.googleapis.com/token'
+        
+        data = {
+            'code': code,
+            'client_id': client_id,
+            'client_secret': client_secret,
+            'redirect_uri': redirect_uri,
+            'grant_type': 'authorization_code'
+        }
+        
+        response = requests.post(token_url, data=data)
+        
+        if response.status_code != 200:
+            _logger.error(f"Token exchange failed: {response.status_code} - {response.text}")
+            raise UserError(_('Failed to exchange authorization code for tokens: %s') % response.text)
+        
+        token_data = response.json()
+        
+        access_token = token_data.get('access_token')
+        refresh_token = token_data.get('refresh_token')
+        expires_in = token_data.get('expires_in', 3600)
+        
+        if not access_token:
+            raise UserError(_('No access token received from Google'))
+        
+        # Calculate expiration time
+        from datetime import datetime, timedelta
+        expires_at = datetime.now() + timedelta(seconds=expires_in)
+        
+        return access_token, refresh_token, expires_at
+
+

+ 7 - 14
google_api/views/res_config_settings_views.xml

@@ -20,41 +20,34 @@
                             <field name="google_api_client_secret" password="True" nolabel="1"/>
                         </div>
                         
-                        <!-- Action Buttons -->
+                        <!-- Action Buttons - Simplified for Admin Configuration -->
                         <div class="mt16 d-flex">
                             <button name="action_validate_credentials" 
                                     string="Validate Credentials" 
                                     type="object" 
                                     class="btn btn-secondary me-2"/>
                             <button name="action_test_google_api_connection" 
-                                    string="Test Connection" 
+                                    string="Test API Connection" 
                                     type="object" 
                                     class="btn btn-primary me-2"/>
-                            <button name="action_connect_google_account" 
-                                    string="Connect Google Account" 
-                                    type="object" 
-                                    class="btn btn-success me-2"/>
-                            <button name="action_test_google_oauth_connection" 
-                                    string="Test OAuth Connection" 
-                                    type="object" 
-                                    class="btn btn-info me-2"/>
                             <button name="action_show_oauth_redirect_uris" 
                                     string="Show Redirect URI" 
                                     type="object" 
                                     class="btn btn-warning"/>
                         </div>
                         
-                        <!-- Help Info -->
+                        <!-- Help Info - Updated -->
                         <div class="mt16">
                             <div class="alert alert-info" role="alert">
                                 <strong>Configuration Steps:</strong>
                                 <ol class="mb-0 mt-2">
                                     <li><strong>Validate Credentials:</strong> Check if credentials are properly formatted</li>
-                                    <li><strong>Test Connection:</strong> Verify connectivity to Google APIs</li>
+                                    <li><strong>Test API Connection:</strong> Verify connectivity to Google APIs</li>
                                     <li><strong>Show Redirect URI:</strong> Get the URL to configure in Google Cloud Console</li>
-                                    <li><strong>Connect Google Account:</strong> Authorize access to Google Drive</li>
-                                    <li><strong>Test OAuth Connection:</strong> Verify the OAuth connection is working</li>
                                 </ol>
+                                <div class="my-2" style="border-top: 1px solid #dee2e6;"></div>
+                                <strong>User Connection:</strong>
+                                <p class="mb-0 mt-2">Users connect their Google accounts from their personal preferences, not from here.</p>
                             </div>
                         </div>
                     </div>

+ 69 - 0
google_api/views/res_users_views.xml

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+
+        
+        <!-- Vista para las preferencias del usuario -->
+        <record id="view_users_preferences_form_google_connect" model="ir.ui.view">
+            <field name="name">res.users.preferences.form.google.connect</field>
+            <field name="model">res.users</field>
+            <field name="inherit_id" ref="base.view_users_form_simple_modif"/>
+            <field name="arch" type="xml">
+                <xpath expr="//group[@name='preference_contact']" position="inside">
+                    <group name="google_connect_group" string="Google Connect" groups="base.group_system">
+                        <field name="res_users_settings_id" invisible="1"/>
+                        
+                        <!-- Google Connection Status -->
+                        <field name="google_connected" readonly="1" 
+                                widget="boolean_toggle" 
+                                options="{'terminology': 'connection'}"/>
+                        
+                        <!-- Google Email (readonly) -->
+                        <field name="google_email" readonly="1" 
+                                invisible="not google_connected"/>
+                        
+                        <!-- CRM Configuration Section -->
+                        <group name="crm_configuration" 
+                                string="CRM" 
+                                invisible="not google_connected">
+                            <field name="res_users_settings_id" invisible="1"/>
+                            <field name="google_crm_meets_folder_id" 
+                                   placeholder="ID de carpeta de Google Drive para archivos meets"
+                                   help="ID de la carpeta en Google Drive donde se almacenan archivos de meets para sincronización automática con CRM"/>
+                        </group>
+                        
+                        <!-- Connection Actions -->
+                        <group name="google_actions" 
+                                invisible="google_connected">
+                            <button name="action_connect_google" 
+                                    type="object" 
+                                    string="Conectar Google" 
+                                    class="btn-primary"
+                                    icon="fa-google"/>
+                        </group>
+                        
+                        <group name="google_actions_connected" 
+                                invisible="not google_connected">
+                            <button name="action_test_google_connection" 
+                                    type="object" 
+                                    string="Probar Conexión" 
+                                    class="btn-secondary"
+                                    icon="fa-refresh"/>
+                            
+                            <button name="action_disconnect_google" 
+                                    type="object" 
+                                    string="Desconectar" 
+                                    class="btn-danger"
+                                    icon="fa-unlink"
+                                    confirm="¿Estás seguro de que quieres desconectar tu cuenta de Google?"/>
+                        </group>
+                        
+
+                        
+
+                    </group>
+                </xpath>
+            </field>
+        </record>
+    </data>
+</odoo>