瀏覽代碼

Merge commit 'bda97a4b6fefa960ceae165f4ae5b6ad13abe92e' as 'google_api'

root 5 月之前
父節點
當前提交
c3827a144f

+ 31 - 0
google_api/README.md

@@ -0,0 +1,31 @@
+# Google API Integration
+
+Módulo simple para agregar configuración de Google API en los ajustes generales de Odoo.
+
+## Instalación
+
+1. Copia el módulo a `extra-addons/custom/google_api/`
+2. Actualiza la lista de módulos en Odoo
+3. Instala el módulo "Google API Integration"
+
+## Configuración
+
+1. Ve a **Ajustes** > **General Settings**
+2. En la sección **Integrations**, encuentra **Google API Integration**
+3. Marca la casilla para habilitar
+4. Completa los campos de Client ID y Client Secret
+
+## Obtención de Credenciales
+
+### Google Cloud Console
+
+1. Ve a [Google Cloud Console](https://console.cloud.google.com/)
+2. Crea un proyecto o selecciona uno existente
+3. Habilita las APIs necesarias:
+   - Google Drive API
+   - Google Calendar API
+4. Ve a "Credentials" > "Create Credentials" > "OAuth 2.0 Client IDs"
+5. Configura una credencial única para todos los servicios
+6. Anota el Client ID y Client Secret
+
+**Nota**: Una sola credencial OAuth 2.0 puede ser usada para múltiples APIs de Google siempre que estén habilitadas en el mismo proyecto.

+ 2 - 0
google_api/__init__.py

@@ -0,0 +1,2 @@
+from . import models
+from . import controllers

+ 25 - 0
google_api/__manifest__.py

@@ -0,0 +1,25 @@
+{
+    'name': 'Google API Integration',
+    'version': '1.0.0',
+    'category': 'Integrations',
+    'summary': 'Integración con Google Drive y Google Calendar',
+    'description': """
+        Módulo de integración con Google APIs que incluye:
+        - Integración con Google Drive para gestión de archivos
+        - Integración con Google Calendar para sincronización de eventos
+        - Configuración centralizada en Ajustes Generales
+    """,
+    'author': 'MC Team',
+    'website': 'https://mcteam.mx',
+    'depends': [
+        'base',
+        'base_setup',
+    ],
+    'data': [
+        'views/res_config_settings_views.xml',
+    ],
+    'installable': True,
+    'application': False,
+    'auto_install': False,
+    'license': 'LGPL-3',
+}

+ 1 - 0
google_api/controllers/__init__.py

@@ -0,0 +1 @@
+from . import main

+ 124 - 0
google_api/controllers/main.py

@@ -0,0 +1,124 @@
+# -*- 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
+
+
+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"""
+        
+        # Get the authorization code from the callback
+        code = kw.get('code')
+        state = kw.get('state')
+        error = kw.get('error')
+        
+        if error:
+            return f"""
+            <html>
+            <head><title>OAuth Error</title></head>
+            <body>
+                <h1>OAuth Error</h1>
+                <p>Error: {error}</p>
+                <p><a href="/web">Return to Odoo</a></p>
+            </body>
+            </html>
+            """
+        
+        if not code:
+            return """
+            <html>
+            <head><title>OAuth Error</title></head>
+            <body>
+                <h1>OAuth Error</h1>
+                <p>No authorization code received</p>
+                <p><a href="/web">Return to Odoo</a></p>
+            </body>
+            </html>
+            """
+        
+        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')
+            
+            if not all([client_id, client_secret, redirect_uri]):
+                raise Exception("Missing OAuth configuration")
+            
+            # 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,
+            }
+            
+            response = requests.post(token_url, data=token_data, timeout=30)
+            
+            if response.status_code != 200:
+                raise Exception(f"Token exchange failed: {response.status_code} - {response.text}")
+            
+            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
+            )
+            
+            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}")
+                
+        except Exception as e:
+            return f"""
+            <html>
+            <head><title>OAuth Error</title></head>
+            <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>
+            </body>
+            </html>
+            """

+ 1 - 0
google_api/models/__init__.py

@@ -0,0 +1 @@
+from . import res_config_settings

+ 283 - 0
google_api/models/res_config_settings.py

@@ -0,0 +1,283 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import requests
+import json
+from odoo import fields, models, _
+from odoo.exceptions import UserError
+
+
+class ResConfigSettings(models.TransientModel):
+    _inherit = 'res.config.settings'
+
+    # Google API Configuration
+    google_api_enabled = fields.Boolean(
+        string='Google API Integration',
+        config_parameter='google_api.enabled',
+        help='Enable integration with Google Drive and Google Calendar'
+    )
+    
+    # Google API Credentials (shared for all services)
+    google_api_client_id = fields.Char(
+        string='Google API Client ID',
+        config_parameter='google_api.client_id',
+        help='Client ID for Google APIs (Drive, Calendar, etc.)'
+    )
+    google_api_client_secret = fields.Char(
+        string='Google API Client Secret',
+        config_parameter='google_api.client_secret',
+        help='Client Secret for Google APIs (Drive, Calendar, etc.)'
+    )
+
+    def action_test_google_api_connection(self):
+        """Test Google API connection by checking if credentials are valid"""
+        self.ensure_one()
+        
+        if not self.google_api_enabled:
+            raise UserError(_('Google API Integration is not enabled'))
+        
+        if not self.google_api_client_id or not self.google_api_client_secret:
+            raise UserError(_('Please provide both Client ID and Client Secret'))
+        
+        try:
+            # Test 1: Verify credentials format
+            if len(self.google_api_client_id) < 10:
+                raise UserError(_('Client ID appears to be invalid (too short)'))
+            
+            if len(self.google_api_client_secret) < 10:
+                raise UserError(_('Client Secret appears to be invalid (too short)'))
+            
+            # Test 2: Try to reach Google APIs (basic connectivity test)
+            google_api_url = "https://www.googleapis.com"
+            response = requests.get(google_api_url, timeout=10)
+            
+            if response.status_code not in [200, 404]:  # 404 is expected for root URL
+                raise UserError(_('Cannot reach Google APIs. Please check your internet connection.'))
+            
+            # Test 3: Check if Drive API is available
+            drive_api_url = "https://www.googleapis.com/drive/v3/about"
+            drive_response = requests.get(drive_api_url, timeout=10)
+            
+            # Drive API requires authentication, so 401 is expected
+            if drive_response.status_code not in [401, 403]:
+                raise UserError(_('Google Drive API is not accessible'))
+            
+            # Test 4: Check if Calendar API is available
+            calendar_api_url = "https://www.googleapis.com/calendar/v3/users/me/calendarList"
+            calendar_response = requests.get(calendar_api_url, timeout=10)
+            
+            # Calendar API requires authentication, so 401 is expected
+            if calendar_response.status_code not in [401, 403]:
+                raise UserError(_('Google Calendar API is not accessible'))
+            
+            # Test 5: Validate OAuth2 endpoints
+            oauth_auth_url = "https://accounts.google.com/o/oauth2/auth"
+            oauth_response = requests.get(oauth_auth_url, timeout=10)
+            
+            if oauth_response.status_code not in [200, 405]:  # 405 Method Not Allowed is expected for GET
+                raise UserError(_('Google OAuth2 endpoints are not accessible'))
+            
+            # All tests passed
+            return {
+                'type': 'ir.actions.client',
+                'tag': 'display_notification',
+                'params': {
+                    'title': _('Success'),
+                    'message': _('Google API connection test successful! All APIs are accessible.'),
+                    'type': 'success',
+                    'sticky': False,
+                }
+            }
+            
+        except requests.exceptions.Timeout:
+            raise UserError(_('Connection timeout. Please check your internet connection.'))
+        except requests.exceptions.ConnectionError:
+            raise UserError(_('Connection error. Please check your internet connection.'))
+        except Exception as e:
+            raise UserError(_('Connection test failed: %s') % str(e))
+
+    def action_validate_credentials(self):
+        """Validate that the credentials are properly formatted"""
+        self.ensure_one()
+        
+        if not self.google_api_client_id:
+            raise UserError(_('Client ID is required'))
+        
+        if not self.google_api_client_secret:
+            raise UserError(_('Client Secret is required'))
+        
+        # Basic format validation
+        if not self.google_api_client_id.endswith('.apps.googleusercontent.com'):
+            raise UserError(_('Client ID should end with .apps.googleusercontent.com'))
+        
+        if len(self.google_api_client_secret) < 20:
+            raise UserError(_('Client Secret appears to be too short'))
+        
+        return {
+            'type': 'ir.actions.client',
+            'tag': 'display_notification',
+            'params': {
+                'title': _('Success'),
+                'message': _('Credentials format is valid!'),
+                'type': 'success',
+                'sticky': False,
+            }
+        }
+
+    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()
+        
+        base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+        redirect_uri = f"{base_url}/web/google_oauth_callback"
+        
+        return {
+            'type': 'ir.actions.client',
+            'tag': 'display_notification',
+            'params': {
+                'title': _('OAuth Redirect URI'),
+                'message': _('Configure this redirect URI in Google Cloud Console:\n\n%s\n\nCopy this URL and add it to your OAuth 2.0 client configuration.') % redirect_uri,
+                'type': 'info',
+                'sticky': True,
+            }
+        }
+
+    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

+ 3 - 0
google_api/static/description/icon.png

@@ -0,0 +1,3 @@
+# Este es un archivo placeholder para el icono del módulo
+# En un entorno real, aquí iría un archivo PNG de 140x140 píxeles
+# con el logo de Google API o un icono representativo

+ 65 - 0
google_api/views/res_config_settings_views.xml

@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <record id="res_config_settings_view_form" model="ir.ui.view">
+        <field name="name">res.config.settings.view.form.inherit.google.api</field>
+        <field name="model">res.config.settings</field>
+        <field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
+        <field name="arch" type="xml">
+            <xpath expr="//block[@name='integration']" position="inside">
+                <setting string="Google API Integration" 
+                         help="Integrate with Google Drive and Google Calendar"
+                         id="google_api_integration_setting">
+                    <field name="google_api_enabled"/>
+                    <div class="content-group" invisible="not google_api_enabled" id="google_api_config">
+                        <div class="mt16 row">
+                            <label for="google_api_client_id" string="Google API Client ID" class="col-3 col-lg-3 o_light_label"/>
+                            <field name="google_api_client_id" nolabel="1"/>
+                        </div>
+                        <div class="mt16 row">
+                            <label for="google_api_client_secret" string="Google API Client Secret" class="col-3 col-lg-3 o_light_label"/>
+                            <field name="google_api_client_secret" password="True" nolabel="1"/>
+                        </div>
+                        
+                        <!-- Action Buttons -->
+                        <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" 
+                                    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 -->
+                        <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>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>
+                        </div>
+                    </div>
+                </setting>
+            </xpath>
+        </field>
+    </record>
+</odoo>