瀏覽代碼

Merge commit 'cb081004e6bc4bae32fb49ee206a20eb439aed38' as 'm22tc_google_workspace'

root 5 月之前
父節點
當前提交
4c423c193e

+ 82 - 0
m22tc_google_workspace/README.md

@@ -0,0 +1,82 @@
+# M22TC Google Workspace
+
+Módulo de integración con Google Workspace para CRM que permite gestionar documentos de oportunidades en Google Drive.
+
+## Características
+
+- **Integración con Google Drive**: Campo para seleccionar folder de Google Drive en la empresa
+- **Gestión de documentos por oportunidad**: Cada oportunidad puede tener su propio folder en Google Drive
+- **Configuración centralizada**: Configuración en la empresa que se aplica a todas las oportunidades
+- **Interfaz intuitiva**: Botones para crear, abrir y gestionar folders de Google Drive
+
+## Dependencias
+
+- `google_api`: Módulo base para integración con Google APIs
+- `crm`: Módulo de CRM de Odoo
+
+## Instalación
+
+1. Asegúrate de que el módulo `google_api` esté instalado y configurado
+2. Instala este módulo desde la lista de módulos de Odoo
+3. Configura las credenciales de Google Drive en la empresa
+
+## Configuración
+
+### 1. Configuración de la Empresa
+
+1. Ve a **Ajustes** > **Empresas** > Selecciona tu empresa
+2. En la pestaña **Google Workspace**:
+   - Marca **Enable Google Drive CRM Integration**
+   - Ingresa el **Google Drive CRM Folder ID** (ID del folder principal)
+   - Haz clic en **Test Google Drive Connection** para verificar
+
+### 2. Obtener Folder ID de Google Drive
+
+1. Ve a [Google Drive](https://drive.google.com/)
+2. Navega al folder que quieres usar como principal para CRM
+3. La URL será: `https://drive.google.com/drive/folders/FOLDER_ID`
+4. Copia el `FOLDER_ID` de la URL
+
+## Uso
+
+### En Oportunidades de CRM
+
+1. Abre una oportunidad de CRM
+2. Ve a la pestaña **Google Drive**
+3. Haz clic en **Create Google Drive Folder** para crear un folder específico
+4. Usa **Open Google Drive Folder** para acceder al folder
+5. Usa **Rename Folder Structure** para renombrar la estructura si cambian los componentes
+6. Usa **Upload Documents** para subir archivos
+
+### Funcionalidades
+
+- **Crear folder**: Crea automáticamente un folder para cada oportunidad
+- **Abrir folder**: Abre el folder de Google Drive en una nueva pestaña
+- **Renombrar estructura**: Renombra las carpetas existentes si cambian los componentes
+- **Contar documentos**: Muestra el número de documentos en el folder
+- **Filtros**: Filtra oportunidades con/sin folder de Google Drive
+
+## Estructura de Folders
+
+```
+Google Drive CRM Folder (Principal)
+├── Oportunidad 1
+│   ├── Documento1.pdf
+│   ├── Documento2.docx
+│   └── ...
+├── Oportunidad 2
+│   ├── Propuesta.pdf
+│   └── ...
+└── ...
+```
+
+## Próximas Funcionalidades
+
+- Sincronización automática de documentos
+- Subida masiva de archivos
+- Integración con Google Calendar para eventos
+- Notificaciones automáticas
+
+## Soporte
+
+Para soporte técnico, contacta a MC Team en [https://mcteam.mx](https://mcteam.mx)

+ 1 - 0
m22tc_google_workspace/__init__.py

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

+ 35 - 0
m22tc_google_workspace/__manifest__.py

@@ -0,0 +1,35 @@
+{
+    'name': 'M22TC Google Workspace',
+    'version': '1.0.0',
+    'category': 'Sales/CRM',
+    'summary': 'Integración de Google Workspace con CRM para M22TC',
+    'description': """
+        Módulo de integración con Google Workspace que incluye:
+        - Campo para seleccionar folder de Google Drive en la empresa
+        - Integración con Google Drive para documentos del CRM
+        - Configuración centralizada en la empresa
+    """,
+    'author': 'MC Team',
+    'website': 'https://mcteam.mx',
+    'depends': [
+        'base',
+        'crm',
+        'google_api',
+    ],
+    'data': [
+        'security/ir.model.access.csv',
+        'views/res_company_views.xml',
+        'views/crm_lead_views.xml',
+        'data/m22tc_google_workspace_data.xml',
+    ],
+    'assets': {
+        'web.assets_backend': [
+            'm22tc_google_workspace/static/src/js/google_drive_widget.js',
+            'm22tc_google_workspace/static/src/css/google_drive_widget.css',
+        ],
+    },
+    'installable': True,
+    'application': True,
+    'auto_install': False,
+    'license': 'LGPL-3',
+}

+ 10 - 0
m22tc_google_workspace/data/m22tc_google_workspace_data.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data noupdate="1">
+        <!-- Default Google Workspace Configuration -->
+        <record id="default_google_drive_crm_enabled" model="ir.config_parameter">
+            <field name="key">m22tc_google_workspace.drive_crm_enabled</field>
+            <field name="value">False</field>
+        </record>
+    </data>
+</odoo>

+ 2 - 0
m22tc_google_workspace/models/__init__.py

@@ -0,0 +1,2 @@
+from . import res_company
+from . import crm_lead

+ 2227 - 0
m22tc_google_workspace/models/crm_lead.py

@@ -0,0 +1,2227 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import requests
+import json
+import logging
+import re
+from datetime import datetime, timedelta
+from odoo import fields, models, api, _
+from odoo.exceptions import UserError, ValidationError
+
+_logger = logging.getLogger(__name__)
+
+# Cache for Google Drive API responses (5 minutes)
+_GOOGLE_DRIVE_CACHE = {}
+_CACHE_TIMEOUT = 300  # 5 minutes
+
+def _clear_google_drive_cache():
+    """Clear expired cache entries"""
+    global _GOOGLE_DRIVE_CACHE
+    current_time = datetime.now()
+    expired_keys = [
+        key for key, entry in _GOOGLE_DRIVE_CACHE.items()
+        if current_time >= entry['expires']
+    ]
+    for key in expired_keys:
+        del _GOOGLE_DRIVE_CACHE[key]
+
+
+class CrmLead(models.Model):
+    _inherit = 'crm.lead'
+
+    google_drive_documents_count = fields.Integer(
+        string='Google Drive Documents',
+        compute='_compute_google_drive_documents_count',
+        help='Number of documents in Google Drive for this opportunity'
+    )
+    google_drive_folder_id = fields.Char(
+        string='Google Drive Folder ID',
+        help='ID del folder específico en Google Drive para esta oportunidad',
+        readonly=True
+    )
+    google_drive_folder_name = fields.Char(
+        string='Google Drive Folder Name',
+        help='Nombre del folder en Google Drive para esta oportunidad',
+        readonly=True
+    )
+    google_drive_url = fields.Char(
+        string='URL Drive',
+        help='URL de la carpeta de Google Drive de la oportunidad'
+    )
+
+    @api.depends('google_drive_folder_id')
+    def _compute_google_drive_documents_count(self):
+        """Compute the number of documents in Google Drive with caching"""
+        for record in self:
+            if record.google_drive_folder_id:
+                # Check cache first
+                cache_key = f'doc_count_{record.google_drive_folder_id}'
+                if cache_key in _GOOGLE_DRIVE_CACHE:
+                    cache_entry = _GOOGLE_DRIVE_CACHE[cache_key]
+                    if datetime.now() < cache_entry['expires']:
+                        record.google_drive_documents_count = cache_entry['count']
+                        continue
+                
+                # TODO: Implement Google Drive API call to count documents
+                # For now, return 0 but cache the result
+                count = 0
+                _GOOGLE_DRIVE_CACHE[cache_key] = {
+                    'count': count,
+                    'expires': datetime.now() + timedelta(seconds=300)  # 5 minutes
+                }
+                record.google_drive_documents_count = count
+            else:
+                record.google_drive_documents_count = 0
+
+    def _get_google_drive_access_token(self):
+        """Get Google Drive access token from system parameters with caching"""
+        # Check cache first
+        cache_key = 'access_token'
+        if cache_key in _GOOGLE_DRIVE_CACHE:
+            cache_entry = _GOOGLE_DRIVE_CACHE[cache_key]
+            if datetime.now() < cache_entry['expires']:
+                return cache_entry['token']
+        
+        access_token = self.env['ir.config_parameter'].sudo().get_param('google_api.access_token')
+        if not access_token:
+            raise UserError(_('No OAuth token found. Please connect your Google account in Google API settings first.'))
+        
+        # Test if the token is still valid
+        if not self._test_access_token(access_token):
+            # Try to refresh the token
+            if self._refresh_access_token():
+                access_token = self.env['ir.config_parameter'].sudo().get_param('google_api.access_token')
+            else:
+                raise UserError(_('Google Drive access token has expired and could not be refreshed. Please reconnect your Google account.'))
+        
+        # Cache the token for 5 minutes
+        _GOOGLE_DRIVE_CACHE[cache_key] = {
+            'token': access_token,
+            'expires': datetime.now() + timedelta(seconds=_CACHE_TIMEOUT)
+        }
+        
+        return access_token
+
+    def _test_access_token(self, access_token):
+        """Test if the access token is still valid with better error handling"""
+        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=10
+            )
+            
+            if response.status_code == 200:
+                return True
+            elif response.status_code == 401:
+                _logger.warning("Google Drive access token is invalid (401)")
+                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]):
+                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
+
+    def _get_company_root_folder_id(self):
+        """Get the company's root Google Drive folder ID"""
+        if not self.company_id or not self.company_id.google_drive_crm_enabled:
+            return None
+        
+        if not self.company_id.google_drive_crm_folder_id:
+            return None
+        
+        return self.company_id.google_drive_crm_folder_id
+
+    def _extract_folder_id_from_url(self, url):
+        """Extract folder ID from Google Drive URL"""
+        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)
+        
+        return None
+
+    def _get_google_drive_field_value(self):
+        """Get value from the configured Google Drive field in company settings"""
+        if not self.company_id.google_drive_crm_field_id:
+            return None
+        
+        field_name = self.company_id.google_drive_crm_field_id.name
+        if not field_name:
+            return None
+        
+        # Get the field value from the record
+        field_value = getattr(self, field_name, None)
+        return field_value
+
+    def _try_extract_folder_id_from_field(self):
+        """Try to extract folder ID from the configured field value"""
+        field_value = self._get_google_drive_field_value()
+        if not field_value:
+            return None
+        
+        # Try to extract folder ID from the field value (could be URL or direct ID)
+        folder_id = self._extract_folder_id_from_url(field_value)
+        if folder_id:
+            return folder_id
+        
+        # If it's not a URL, check if it looks like a folder ID
+        if isinstance(field_value, str) and len(field_value) >= 10 and len(field_value) <= 50:
+            # Basic validation for Google Drive folder ID format
+            import re
+            if re.match(r'^[a-zA-Z0-9_-]+$', field_value):
+                return field_value
+        
+        return None
+
+    def _validate_folder_creation_prerequisites(self):
+        """Validate prerequisites before creating Google Drive folder"""
+        # Check if company has Google Drive folder configured
+        root_folder_id = self._get_company_root_folder_id()
+        if not root_folder_id:
+            self.message_post(
+                body=_("⚠️ Google Drive folder creation skipped: Company doesn't have Google Drive configured."),
+                message_type='comment'
+            )
+            return False
+        
+        # Validate contact exists
+        if not self.partner_id:
+            self.message_post(
+                body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity. Please assign a contact before creating Google Drive folders."),
+                message_type='comment'
+            )
+            return False
+        
+        return True
+
+    def _try_get_existing_folder_id(self):
+        """Try to get existing folder ID from various sources"""
+        # First, check if we already have a folder ID
+        if self.google_drive_folder_id:
+            return self.google_drive_folder_id
+        
+        # Second, try to extract from the configured field
+        field_folder_id = self._try_extract_folder_id_from_field()
+        if field_folder_id:
+            _logger.info(f"Found folder ID from configured field: {field_folder_id}")
+            return field_folder_id
+        
+        # Third, try to extract from google_drive_url field
+        if self.google_drive_url:
+            url_folder_id = self._extract_folder_id_from_url(self.google_drive_url)
+            if url_folder_id:
+                _logger.info(f"Found folder ID from URL field: {url_folder_id}")
+                return url_folder_id
+        
+        return None
+
+    def _check_folder_company_mismatch(self):
+        """Check if the current folder belongs to the correct company"""
+        if not self.google_drive_folder_id or not self.company_id:
+            return False
+        
+        try:
+            # Get access token
+            access_token = self._get_google_drive_access_token()
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+            
+            # Get company root folder ID
+            company_root_folder_id = self._get_company_root_folder_id()
+            if not company_root_folder_id:
+                return False
+            
+            # Get current folder info
+            response = requests.get(
+                f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}?fields=parents',
+                headers=headers,
+                timeout=30
+            )
+            
+            if response.status_code != 200:
+                return False
+            
+            folder_data = response.json()
+            parent_ids = folder_data.get('parents', [])
+            
+            if not parent_ids:
+                return False
+            
+            # Navigate up to find if the folder is under the correct company root
+            current_parent_id = parent_ids[0]
+            max_levels = 5  # Prevent infinite loop
+            
+            for _ in range(max_levels):
+                # Check if current parent is the company root folder
+                if current_parent_id == company_root_folder_id:
+                    # Folder is under the correct company root
+                    return False
+                
+                parent_response = requests.get(
+                    f'https://www.googleapis.com/drive/v3/files/{current_parent_id}?fields=parents',
+                    headers=headers,
+                    timeout=30
+                )
+                
+                if parent_response.status_code != 200:
+                    break
+                
+                parent_data = parent_response.json()
+                parent_parent_ids = parent_data.get('parents', [])
+                
+                if not parent_parent_ids:
+                    # Reached the top level (My Drive), folder is not under the correct company root
+                    _logger.info(f"Folder belongs to wrong company. Current root: {current_parent_id}, Expected: {company_root_folder_id}")
+                    return True
+                
+                current_parent_id = parent_parent_ids[0]
+            
+            # If we reach here, folder is not under the correct company root
+            _logger.info(f"Folder belongs to wrong company. Could not find company root: {company_root_folder_id}")
+            return True
+            
+        except Exception as e:
+            _logger.error(f"Error checking folder company mismatch: {str(e)}")
+            return False
+
+    def _get_folder_name_components(self):
+        """Get the components for folder naming with priority for partner_id"""
+        primary_name = None
+        
+        if self.partner_id:
+            _logger.info(f"Contact found: {self.partner_id.name} (ID: {self.partner_id.id})")
+            
+            # Prioridad 1: partner_id.company_id.name
+            if self.partner_id.company_id and self.partner_id.company_id.name:
+                primary_name = self.partner_id.company_id.name
+                _logger.info(f"Using company_id.name: {primary_name}")
+            # Prioridad 2: partner_id.company_name
+            elif self.partner_id.company_name:
+                primary_name = self.partner_id.company_name
+                _logger.info(f"Using company_name: {primary_name}")
+            # Prioridad 3: partner_id.name
+            else:
+                primary_name = self.partner_id.name
+                _logger.info(f"Using partner name: {primary_name}")
+        else:
+            _logger.warning("No contact assigned to opportunity")
+            primary_name = "Sin Contacto"
+        
+        if not primary_name:
+            raise UserError(_('No company or contact name available. Please assign a contact with company information before creating Google Drive folders.'))
+        
+        # Validate and sanitize the primary name
+        sanitized_primary_name = self._sanitize_folder_name(primary_name)
+        sanitized_opportunity_name = self._sanitize_folder_name(self.name)
+        year = str(self.create_date.year) if self.create_date else str(datetime.now().year)
+        
+        _logger.info(f"Folder components - Primary: '{sanitized_primary_name}', Opportunity: '{sanitized_opportunity_name}', Year: {year}")
+        
+        return {
+            'primary_name': sanitized_primary_name,
+            'opportunity_name': sanitized_opportunity_name,
+            'year': year
+        }
+
+    def _check_partner_name_changes(self, vals):
+        """Check if partner or partner's company name has changed"""
+        if 'partner_id' not in vals:
+            return False
+        
+        old_partner = self.partner_id
+        new_partner_id = vals['partner_id']
+        
+        if not new_partner_id:
+            return False
+        
+        new_partner = self.env['res.partner'].browse(new_partner_id)
+        
+        # Check if partner changed
+        if old_partner.id != new_partner.id:
+            return True
+        
+        # Check if partner's company name changed
+        old_company_name = old_partner.parent_id.name if old_partner.parent_id else old_partner.name
+        new_company_name = new_partner.parent_id.name if new_partner.parent_id else new_partner.name
+        
+        return old_company_name != new_company_name
+
+    def _sanitize_folder_name(self, name):
+        """Sanitize folder name to be Google Drive compatible with optimization"""
+        if not name:
+            return 'Sin nombre'
+        
+        # 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 _validate_folder_id_with_google_drive(self, folder_id):
+        """Validate if the folder ID exists and is accessible in Google Drive"""
+        try:
+            access_token = self._get_google_drive_access_token()
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+            
+            # Test access to the specific folder
+            response = requests.get(
+                f'https://www.googleapis.com/drive/v3/files/{folder_id}?fields=id,name,mimeType',
+                headers=headers,
+                timeout=10
+            )
+            
+            if response.status_code == 200:
+                folder_data = response.json()
+                if folder_data.get('mimeType') == 'application/vnd.google-apps.folder':
+                    _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
+                    return True, folder_data.get('name', 'Unknown')
+                else:
+                    _logger.warning(f"❌ ID {folder_id} exists but is not a folder")
+                    return False, "Not a folder"
+            elif response.status_code == 404:
+                _logger.warning(f"❌ Folder ID {folder_id} not found in Google Drive")
+                return False, "Not found"
+            elif response.status_code == 403:
+                _logger.warning(f"❌ Access denied to folder ID {folder_id}")
+                return False, "Access denied"
+            elif response.status_code == 401:
+                _logger.error(f"❌ OAuth token expired or invalid")
+                return False, "Authentication error"
+            else:
+                _logger.warning(f"❌ Google Drive API error: {response.status_code}")
+                return False, f"API error: {response.status_code}"
+                
+        except requests.exceptions.Timeout:
+            _logger.error(f"❌ Timeout validating folder ID {folder_id}")
+            return False, "Timeout"
+        except requests.exceptions.ConnectionError:
+            _logger.error(f"❌ Connection error validating folder ID {folder_id}")
+            return False, "Connection error"
+        except Exception as e:
+            _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
+            return False, str(e)
+
+    def _create_google_drive_folder_structure(self):
+        """Create the complete Google Drive folder structure for this opportunity with optimization"""
+        self.ensure_one()
+        
+        # Validate prerequisites
+        if not self._validate_folder_creation_prerequisites():
+            return None
+        
+        # Try to get existing folder ID first
+        existing_folder_id = self._try_get_existing_folder_id()
+        if existing_folder_id:
+            _logger.info(f"Found existing folder ID: {existing_folder_id}")
+            
+            # Validate the folder ID against Google Drive
+            is_valid, error_message = self._validate_folder_id_with_google_drive(existing_folder_id)
+            
+            if is_valid:
+                _logger.info(f"✅ Folder ID {existing_folder_id} validated successfully")
+                # Update the record with the existing folder ID
+                self.with_context(skip_google_drive_update=True).write({
+                    'google_drive_folder_id': existing_folder_id
+                })
+                
+                # Get expected structure and store it
+                expected_components = self._get_folder_name_components()
+                expected_structure = self._build_structure_string(expected_components)
+                self.with_context(skip_google_drive_update=True).write({
+                    'google_drive_folder_name': expected_structure
+                })
+                
+                self.message_post(
+                    body=_("✅ Using existing Google Drive folder: %s") % existing_folder_id,
+                    message_type='comment'
+                )
+                return True
+            else:
+                _logger.warning(f"❌ Folder ID {existing_folder_id} validation failed: {error_message}")
+                self.message_post(
+                    body=_("⚠️ Folder ID from configured field is not accessible: %s. Creating new folder structure.") % error_message,
+                    message_type='comment'
+                )
+                # Continue to create new folder structure
+        
+        access_token = self._get_google_drive_access_token()
+        components = self._get_folder_name_components()
+        
+        headers = {
+            'Authorization': f'Bearer {access_token}',
+            'Content-Type': 'application/json'
+        }
+        
+        try:
+            # Create folder structure in batch for better performance
+            folder_structure = self._create_folder_structure_batch(headers, components)
+            
+            # Update the opportunity with the main folder ID
+            self.with_context(skip_google_drive_update=True).write({
+                'google_drive_folder_id': folder_structure['opportunity_folder_id'],
+                'google_drive_folder_name': self._build_structure_string(components)
+            })
+            
+            return folder_structure
+            
+        except Exception as e:
+            _logger.error(f"Failed to create folder structure for opportunity {self.id}: {str(e)}")
+            raise UserError(_('Failed to create Google Drive folder structure: %s') % str(e))
+
+    def _create_folder_structure_batch(self, headers, components):
+        """Create folder structure in batch for better performance"""
+        root_folder_id = self._get_company_root_folder_id()
+        
+        # Step 1: Create or get primary folder (Company/Contact)
+        primary_folder_id = self._create_or_get_folder(
+            headers, root_folder_id, components['primary_name']
+        )
+        
+        # Step 2: Create or get year folder
+        year_folder_id = self._create_or_get_folder(
+            headers, primary_folder_id, components['year']
+        )
+        
+        # Step 3: Create or get opportunity folder
+        opportunity_folder_id = self._create_or_get_folder(
+            headers, year_folder_id, components['opportunity_name']
+        )
+        
+        # Step 4: Create Meets and Archivos cliente folders (parallel creation)
+        meets_folder_id = self._create_or_get_folder(
+            headers, opportunity_folder_id, 'Meets'
+        )
+        
+        archivos_folder_id = self._create_or_get_folder(
+            headers, opportunity_folder_id, 'Archivos cliente'
+        )
+        
+        return {
+            'opportunity_folder_id': opportunity_folder_id,
+            'meets_folder_id': meets_folder_id,
+            'archivos_folder_id': archivos_folder_id
+        }
+
+    def _create_or_get_folder(self, headers, parent_folder_id, folder_name):
+        """Create a folder or get existing one by name"""
+        # First, check if folder already exists
+        existing_folder = self._find_folder_by_name(headers, parent_folder_id, folder_name)
+        if existing_folder:
+            return existing_folder['id']
+        
+        # Create new folder
+        folder_metadata = {
+            'name': folder_name,
+            'mimeType': 'application/vnd.google-apps.folder',
+            'parents': [parent_folder_id]
+        }
+        
+        response = requests.post(
+            'https://www.googleapis.com/drive/v3/files',
+            headers=headers,
+            json=folder_metadata,
+            timeout=30
+        )
+        
+        if response.status_code == 200:
+            folder_data = response.json()
+            return folder_data['id']
+        else:
+            raise UserError(_('Failed to create Google Drive folder "%s". Status: %s') % (folder_name, response.status_code))
+
+    def _find_folder_by_name(self, headers, parent_folder_id, folder_name):
+        """Find a folder by name in the parent folder with caching"""
+        # Check cache first
+        cache_key = f'folder_{parent_folder_id}_{folder_name}'
+        if cache_key in _GOOGLE_DRIVE_CACHE:
+            cache_entry = _GOOGLE_DRIVE_CACHE[cache_key]
+            if datetime.now() < cache_entry['expires']:
+                return cache_entry['result']
+        
+        params = {
+            'q': f"'{parent_folder_id}' in parents and name='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false",
+            'fields': 'files(id,name)',
+            'pageSize': 1
+        }
+        
+        try:
+            response = requests.get(
+                'https://www.googleapis.com/drive/v3/files',
+                headers=headers,
+                params=params,
+                timeout=30
+            )
+            
+            if response.status_code == 200:
+                data = response.json()
+                folders = data.get('files', [])
+                result = folders[0] if folders else None
+                
+                # Cache the result for 2 minutes
+                _GOOGLE_DRIVE_CACHE[cache_key] = {
+                    'result': result,
+                    'expires': datetime.now() + timedelta(seconds=120)
+                }
+                
+                return result
+            else:
+                _logger.warning(f"Failed to find folder '{folder_name}' in parent {parent_folder_id}. Status: {response.status_code}")
+                return None
+                
+        except Exception as e:
+            _logger.error(f"Error finding folder '{folder_name}' in parent {parent_folder_id}: {str(e)}")
+            return None
+
+    def _rename_google_drive_folder(self, new_name):
+        """Rename the Google Drive folder with optimization"""
+        if not self.google_drive_folder_id:
+            return
+        
+        # Sanitize the new name
+        sanitized_name = self._sanitize_folder_name(new_name)
+        
+        # Check if the name is actually different
+        if self.google_drive_folder_name == sanitized_name:
+            return
+        
+        access_token = self._get_google_drive_access_token()
+        headers = {
+            'Authorization': f'Bearer {access_token}',
+            'Content-Type': 'application/json'
+        }
+        
+        try:
+            folder_metadata = {
+                'name': sanitized_name
+            }
+            
+            _logger.info(f"Renaming Google Drive folder {self.google_drive_folder_id} to '{sanitized_name}'")
+            
+            response = requests.patch(
+                f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}',
+                headers=headers,
+                json=folder_metadata,
+                timeout=30
+            )
+            
+            if response.status_code == 200:
+                # Update the folder name in Odoo (with context to prevent loop)
+                self.with_context(skip_google_drive_update=True).write({'google_drive_folder_name': sanitized_name})
+                _logger.info(f"Successfully renamed Google Drive folder to '{sanitized_name}'")
+                
+                # Clear cache for this folder
+                cache_key = f'folder_{self.google_drive_folder_id}'
+                if cache_key in _GOOGLE_DRIVE_CACHE:
+                    del _GOOGLE_DRIVE_CACHE[cache_key]
+                    
+            else:
+                error_msg = f'Failed to rename Google Drive folder. Status: {response.status_code}'
+                if response.text:
+                    error_msg += f' - Response: {response.text}'
+                _logger.error(error_msg)
+                raise UserError(_(error_msg))
+                
+        except requests.exceptions.Timeout:
+            raise UserError(_('Timeout while renaming Google Drive folder. Please try again.'))
+        except requests.exceptions.ConnectionError:
+            raise UserError(_('Connection error while renaming Google Drive folder. Please check your internet connection.'))
+        except Exception as e:
+            _logger.error(f"Error renaming folder {self.google_drive_folder_id}: {str(e)}")
+            raise UserError(_('Failed to rename Google Drive folder: %s') % str(e))
+
+    def _move_google_drive_folder(self, new_company_id):
+        """Move the Google Drive folder to a new company's structure"""
+        if not self.google_drive_folder_id:
+            return
+        
+        # Get new company's root folder
+        new_root_folder_id = new_company_id.google_drive_crm_folder_id
+        if not new_root_folder_id:
+            raise UserError(_('New company does not have Google Drive CRM folder configured'))
+        
+        access_token = self._get_google_drive_access_token()
+        headers = {
+            'Authorization': f'Bearer {access_token}',
+            'Content-Type': 'application/json'
+        }
+        
+        # Get current folder's parent
+        response = requests.get(
+            f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}?fields=parents',
+            headers=headers,
+            timeout=30
+        )
+        
+        if response.status_code != 200:
+            raise UserError(_('Failed to get current folder information'))
+        
+        folder_data = response.json()
+        current_parents = folder_data.get('parents', [])
+        
+        # Remove from current parent and add to new parent
+        if current_parents:
+            # Remove from current parent
+            remove_response = requests.delete(
+                f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}/parents/{current_parents[0]}',
+                headers=headers,
+                timeout=30
+            )
+            
+            if remove_response.status_code != 204:
+                raise UserError(_('Failed to remove folder from current parent'))
+        
+        # Add to new parent
+        add_response = requests.post(
+            f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}/parents',
+            headers=headers,
+            json={'id': new_root_folder_id},
+            timeout=30
+        )
+        
+        if add_response.status_code != 200:
+            raise UserError(_('Failed to move folder to new company'))
+
+    def _delete_google_drive_folder_structure(self):
+        """Delete the Google Drive folder structure when contact is removed"""
+        if not self.google_drive_folder_id:
+            return
+        
+        access_token = self._get_google_drive_access_token()
+        headers = {
+            'Authorization': f'Bearer {access_token}',
+        }
+        
+        # Delete the folder (this will also delete subfolders)
+        response = requests.delete(
+            f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}',
+            headers=headers,
+            timeout=30
+        )
+        
+        if response.status_code == 204:
+            # Clear the folder ID from the record
+            self.write({
+                'google_drive_folder_id': False,
+                'google_drive_folder_name': False
+            })
+        else:
+            raise UserError(_('Failed to delete Google Drive folder structure'))
+
+    def _recreate_google_drive_folder_structure(self):
+        """Recreate the Google Drive folder structure when contact changes"""
+        if not self.partner_id:
+            raise UserError(_('No contact associated with this opportunity. Cannot recreate folder structure.'))
+        
+        # Store old folder information for reference
+        old_folder_id = self.google_drive_folder_id
+        old_folder_name = self.google_drive_folder_name
+        
+        # Clear the folder ID but don't delete the actual folder
+        self.write({
+            'google_drive_folder_id': False,
+            'google_drive_folder_name': False
+        })
+        
+        # Create new structure
+        try:
+            new_structure = self._create_google_drive_folder_structure()
+            
+            # Log the recreation for audit purposes
+            if old_folder_id:
+                _logger.info(f"Recreated Google Drive folder structure for opportunity {self.id}: "
+                           f"Old folder: {old_folder_id} ({old_folder_name}) -> "
+                           f"New folder: {self.google_drive_folder_id} ({self.google_drive_folder_name})")
+            
+            return new_structure
+        except Exception as e:
+            # Restore old values if recreation fails
+            self.write({
+                'google_drive_folder_id': old_folder_id,
+                'google_drive_folder_name': old_folder_name
+            })
+            raise
+
+    def _rename_entire_folder_structure(self, old_components, new_components):
+        """Rename the entire folder structure instead of recreating it"""
+        if not self.google_drive_folder_id:
+            return
+        
+        access_token = self._get_google_drive_access_token()
+        headers = {
+            'Authorization': f'Bearer {access_token}',
+            'Content-Type': 'application/json'
+        }
+        
+        # Get the current folder path to understand the structure
+        current_folder_id = self.google_drive_folder_id
+        
+        # Get folder information to find parent folders
+        response = requests.get(
+            f'https://www.googleapis.com/drive/v3/files/{current_folder_id}?fields=parents,name',
+            headers=headers,
+            timeout=30
+        )
+        
+        if response.status_code != 200:
+            raise UserError(_('Failed to get current folder information'))
+        
+        folder_data = response.json()
+        parent_ids = folder_data.get('parents', [])
+        
+        if not parent_ids:
+            raise UserError(_('Cannot rename folder structure: no parent folder found'))
+        
+        # Navigate up the hierarchy to find the primary folder (company/contact)
+        primary_folder_id = self._find_primary_folder_id(headers, current_folder_id)
+        if not primary_folder_id:
+            raise UserError(_('Cannot find primary folder in the structure'))
+        
+        # Rename the primary folder (company/contact name)
+        if old_components['primary_name'] != new_components['primary_name']:
+            self._rename_folder(headers, primary_folder_id, new_components['primary_name'])
+        
+        # Find and rename year folder if needed
+        if old_components['year'] != new_components['year']:
+            year_folder_id = self._find_year_folder_id(headers, primary_folder_id, old_components['year'])
+            if year_folder_id:
+                self._rename_folder(headers, year_folder_id, new_components['year'])
+        
+        # Rename opportunity folder if needed
+        if old_components['opportunity_name'] != new_components['opportunity_name']:
+            _logger.info(f"Renaming opportunity folder from '{old_components['opportunity_name']}' to '{new_components['opportunity_name']}'")
+            self._rename_folder(headers, current_folder_id, new_components['opportunity_name'])
+            # Update the folder name in Odoo (with context to prevent loop)
+            self.with_context(skip_google_drive_update=True).write({'google_drive_folder_name': new_components['opportunity_name']})
+        else:
+            _logger.info(f"Opportunity folder name is already correct: '{new_components['opportunity_name']}'")
+
+    def _find_primary_folder_id(self, headers, current_folder_id):
+        """Find the primary folder (company/contact) by navigating up the hierarchy"""
+        max_depth = 5  # Prevent infinite loops
+        current_id = current_folder_id
+        
+        for depth in range(max_depth):
+            response = requests.get(
+                f'https://www.googleapis.com/drive/v3/files/{current_id}?fields=parents,name',
+                headers=headers,
+                timeout=30
+            )
+            
+            if response.status_code != 200:
+                return None
+            
+            folder_data = response.json()
+            parent_ids = folder_data.get('parents', [])
+            
+            if not parent_ids:
+                # This is the root folder, go back one level
+                return current_id
+            
+            # Check if this folder is the primary folder (not a year or opportunity folder)
+            # Primary folder is typically the company/contact name
+            folder_name = folder_data.get('name', '')
+            if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
+                # This could be the primary folder, but let's check if it has a year folder as child
+                year_folders = self._find_folders_by_name(headers, current_id, r'^\d{4}$')
+                if year_folders:
+                    # This is the primary folder
+                    return current_id
+            
+            current_id = parent_ids[0]
+        
+        return None
+
+    def _find_year_folder_id(self, headers, primary_folder_id, year):
+        """Find the year folder within the primary folder"""
+        year_folders = self._find_folders_by_name(headers, primary_folder_id, f'^{year}$')
+        return year_folders[0]['id'] if year_folders else None
+
+    def _find_folders_by_name(self, headers, parent_id, name_pattern):
+        """Find folders by name pattern in a parent folder"""
+        import re
+        
+        params = {
+            'q': f"'{parent_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false",
+            'fields': 'files(id,name)',
+            'pageSize': 100
+        }
+        
+        response = requests.get(
+            'https://www.googleapis.com/drive/v3/files',
+            headers=headers,
+            params=params,
+            timeout=30
+        )
+        
+        if response.status_code != 200:
+            return []
+        
+        data = response.json()
+        folders = data.get('files', [])
+        
+        # Filter by name pattern
+        pattern = re.compile(name_pattern)
+        return [folder for folder in folders if pattern.match(folder.get('name', ''))]
+
+    def _rename_folder(self, headers, folder_id, new_name):
+        """Rename a specific folder"""
+        _logger.info(f"Attempting to rename folder {folder_id} to '{new_name}'")
+        
+        folder_metadata = {
+            'name': new_name
+        }
+        
+        try:
+            response = requests.patch(
+                f'https://www.googleapis.com/drive/v3/files/{folder_id}',
+                headers=headers,
+                json=folder_metadata,
+                timeout=30
+            )
+            
+            if response.status_code == 200:
+                _logger.info(f"Successfully renamed folder {folder_id} to '{new_name}'")
+            else:
+                _logger.error(f"Failed to rename folder {folder_id}. Status: {response.status_code}, Response: {response.text}")
+                raise UserError(_('Failed to rename folder "%s" to "%s". Status: %s') % (folder_id, new_name, response.status_code))
+                
+        except requests.exceptions.Timeout:
+            _logger.error(f"Timeout while renaming folder {folder_id}")
+            raise UserError(_('Timeout while renaming folder "%s"') % folder_id)
+        except requests.exceptions.ConnectionError:
+            _logger.error(f"Connection error while renaming folder {folder_id}")
+            raise UserError(_('Connection error while renaming folder "%s"') % folder_id)
+        except Exception as e:
+            _logger.error(f"Error renaming folder {folder_id}: {str(e)}")
+            raise UserError(_('Failed to rename folder "%s" to "%s": %s') % (folder_id, new_name, str(e)))
+
+    def _update_google_drive_folder_structure(self, vals):
+        """Update the Google Drive folder structure based on changes"""
+        if not self.google_drive_folder_id:
+            return
+        
+        # Check if company has Google Drive folder configured
+        root_folder_id = self._get_company_root_folder_id()
+        if not root_folder_id:
+            # Company doesn't have Google Drive configured, do nothing
+            return
+        
+        # Get current folder information
+        access_token = self._get_google_drive_access_token()
+        headers = {
+            'Authorization': f'Bearer {access_token}',
+            'Content-Type': 'application/json'
+        }
+        
+        # Check if company changed - move folder to new company structure
+        if 'company_id' in vals and vals['company_id'] != self.company_id.id:
+            new_company = self.env['res.company'].browse(vals['company_id'])
+            if new_company.google_drive_crm_enabled and new_company.google_drive_crm_folder_id:
+                _logger.info(f"Company changed from {self.company_id.name} to {new_company.name}. Moving Google Drive folder structure.")
+                self._move_google_drive_folder(new_company)
+            else:
+                # If new company doesn't have Google Drive configured, keep the folder but log it
+                self.message_post(
+                    body=_("⚠️ Company changed to one without Google Drive configuration. Existing folder structure remains unchanged."),
+                    message_type='comment'
+                )
+                return
+        
+        # Check if contact changed - this requires recreating the entire structure
+        if 'partner_id' in vals:
+            if not vals['partner_id']:
+                # Contact was removed, but we don't delete - just log it
+                self.message_post(
+                    body=_("⚠️ Contact was removed from opportunity. Google Drive folder structure remains unchanged."),
+                    message_type='comment'
+                )
+                return
+            else:
+                # Contact changed, recreate the entire structure
+                _logger.info(f"Contact changed. Recreating entire Google Drive folder structure.")
+                self._recreate_google_drive_folder_structure()
+                return
+        
+        # Check if name changed - rename the opportunity folder
+        if 'name' in vals and vals['name'] != self.name:
+            _logger.info(f"Name changed from '{self.name}' to '{vals['name']}'. Renaming Google Drive folder.")
+            self._rename_google_drive_folder(vals['name'])
+        
+        # Validate and update entire folder structure if needed (only for non-name changes)
+        # This will handle changes in company name, contact name, or year
+        if 'partner_id' in vals or 'create_date' in vals:
+            self._validate_and_update_folder_structure(vals)
+        
+        # Check if stage changed and we need to create folder
+        if 'stage_id' in vals:
+            if self.company_id.google_drive_crm_enabled and self.company_id.google_drive_crm_stage_id:
+                if vals['stage_id'] == self.company_id.google_drive_crm_stage_id.id and not self.google_drive_folder_id:
+                    # Check if company has Google Drive folder configured
+                    root_folder_id = self._get_company_root_folder_id()
+                    if not root_folder_id:
+                        # Company doesn't have Google Drive configured, do nothing
+                        return
+                    
+                    # Validate contact exists before attempting to create folder
+                    if not self.partner_id:
+                        self.message_post(
+                            body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity. Please assign a contact before creating Google Drive folders."),
+                            message_type='comment'
+                        )
+                    else:
+                        try:
+                            self._create_google_drive_folder_structure()
+                        except Exception as e:
+                            _logger.error(f"Failed to create Google Drive folder for opportunity {self.id}: {str(e)}")
+                            self.message_post(
+                                body=_("⚠️ Google Drive folder creation failed: %s") % str(e),
+                                message_type='comment'
+                            )
+    
+    def _validate_and_update_folder_structure(self, vals):
+        """Validate and update the entire folder structure if any component changed"""
+        if not self.google_drive_folder_id:
+            return
+        
+        # Get current and new components
+        current_components = self._get_folder_name_components()
+        
+        # Create a temporary record with new values to get new components
+        temp_vals = {}
+        if 'name' in vals:
+            temp_vals['name'] = vals['name']
+        if 'partner_id' in vals:
+            temp_vals['partner_id'] = vals['partner_id']
+        if 'create_date' in vals:
+            temp_vals['create_date'] = vals['create_date']
+        
+        # Create a temporary record to get new components
+        temp_record = self.with_context(skip_google_drive_update=True)
+        for field, value in temp_vals.items():
+            setattr(temp_record, field, value)
+        
+        try:
+            new_components = temp_record._get_folder_name_components()
+        except:
+            # If we can't get new components, skip validation
+            return
+        
+        # Check if any component changed
+        components_changed = (
+            current_components['primary_name'] != new_components['primary_name'] or
+            current_components['year'] != new_components['year']
+        )
+        
+        # Also check if partner name changed (even if same partner, name might have changed)
+        partner_name_changed = self._check_partner_name_changes(vals)
+        
+        if components_changed or partner_name_changed:
+            _logger.info(f"Folder structure components changed. Renaming entire structure.")
+            _logger.info(f"Old components: {current_components}")
+            _logger.info(f"New components: {new_components}")
+            
+            # Store the old folder information for reference
+            old_folder_id = self.google_drive_folder_id
+            old_folder_name = self.google_drive_folder_name
+            old_structure = f"{current_components['primary_name']}/{current_components['year']}/{current_components['opportunity_name']}"
+            new_structure = f"{new_components['primary_name']}/{new_components['year']}/{new_components['opportunity_name']}"
+            
+            # Rename the entire folder structure instead of recreating
+            try:
+                self._rename_entire_folder_structure(current_components, new_components)
+                
+                # Log the change for audit
+                self.message_post(
+                    body=_("🔄 Google Drive folder structure renamed due to changes:<br/>"
+                          "• Old structure: %s<br/>"
+                          "• New structure: %s<br/>"
+                          "• Folder ID: %s (same folder, renamed)") % (
+                        old_structure,
+                        new_structure,
+                        self.google_drive_folder_id
+                    ),
+                    message_type='comment'
+                )
+                
+            except Exception as e:
+                _logger.error(f"Failed to rename folder structure: {str(e)}")
+                self.message_post(
+                    body=_("❌ Failed to rename Google Drive folder structure: %s") % str(e),
+                    message_type='comment'
+                )
+                raise
+            return
+        
+        # Check if company has Google Drive folder configured
+        root_folder_id = self._get_company_root_folder_id()
+        if not root_folder_id:
+            # Company doesn't have Google Drive configured, do nothing
+            return
+        
+        # Get current folder information
+        access_token = self._get_google_drive_access_token()
+        headers = {
+            'Authorization': f'Bearer {access_token}',
+            'Content-Type': 'application/json'
+        }
+        
+        # Check if company changed - move folder to new company structure
+        if 'company_id' in vals and vals['company_id'] != self.company_id.id:
+            new_company = self.env['res.company'].browse(vals['company_id'])
+            if new_company.google_drive_crm_enabled and new_company.google_drive_crm_folder_id:
+                _logger.info(f"Company changed from {self.company_id.name} to {new_company.name}. Moving Google Drive folder structure.")
+                self._move_google_drive_folder(new_company)
+            else:
+                # If new company doesn't have Google Drive configured, keep the folder but log it
+                self.message_post(
+                    body=_("⚠️ Company changed to one without Google Drive configuration. Existing folder structure remains unchanged."),
+                    message_type='comment'
+                )
+                return
+        
+        # Check if contact changed - this requires recreating the entire structure
+        if 'partner_id' in vals:
+            if not vals['partner_id']:
+                # Contact was removed, but we don't delete - just log it
+                self.message_post(
+                    body=_("⚠️ Contact was removed from opportunity. Google Drive folder structure remains unchanged."),
+                    message_type='comment'
+                )
+                return
+            else:
+                # Contact changed, recreate the entire structure
+                _logger.info(f"Contact changed. Recreating entire Google Drive folder structure.")
+                self._recreate_google_drive_folder_structure()
+                return
+        
+        # Check if name changed - rename the opportunity folder
+        if 'name' in vals and vals['name'] != self.name:
+            _logger.info(f"Name changed from '{self.name}' to '{vals['name']}'. Renaming Google Drive folder.")
+            self._rename_google_drive_folder(vals['name'])
+        
+        # Validate and update entire folder structure if needed
+        self._validate_and_update_folder_structure(vals)
+        
+        # Check if stage changed and we need to create folder
+        if 'stage_id' in vals:
+            if self.company_id.google_drive_crm_enabled and self.company_id.google_drive_crm_stage_id:
+                if vals['stage_id'] == self.company_id.google_drive_crm_stage_id.id and not self.google_drive_folder_id:
+                    if self.partner_id:
+                        self._create_google_drive_folder_structure()
+                    else:
+                        self.message_post(
+                            body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity."),
+                            message_type='comment'
+                        )
+
+    @api.model
+    def create(self, vals):
+        """Override create to handle Google Drive folder creation with optimization"""
+        record = super().create(vals)
+        
+        # Check if we should create Google Drive folder (optimized conditions)
+        if (record.company_id.google_drive_crm_enabled and 
+            record.company_id.google_drive_crm_stage_id and
+            record.stage_id.id == record.company_id.google_drive_crm_stage_id.id):
+            
+            # Validate prerequisites before creating folder
+            if record._validate_folder_creation_prerequisites():
+                try:
+                    record._create_google_drive_folder_structure()
+                    
+                    # Store the initial structure and update URL
+                    if record._store_initial_structure_and_update_url():
+                        record.message_post(
+                            body=_("✅ Google Drive folder created automatically"),
+                            message_type='comment'
+                        )
+                except Exception as e:
+                    # Log error but don't fail record creation
+                    _logger.error(f"Failed to create Google Drive folder for opportunity {record.id}: {str(e)}")
+                    record.message_post(
+                        body=_("⚠️ Google Drive folder creation failed: %s") % str(e),
+                        message_type='comment'
+                    )
+        
+        return record
+
+    def write(self, vals):
+        """Override write method to handle Google Drive folder updates"""
+        # Skip Google Drive updates if this is an internal update (to prevent loops)
+        if self.env.context.get('skip_google_drive_update'):
+            return super().write(vals)
+        
+        # Clear cache before processing
+        _clear_google_drive_cache()
+        
+        # Check if any relevant fields are being updated
+        relevant_fields = ['name', 'partner_id', 'create_date', 'stage_id', 'company_id']
+        needs_update = any(field in vals for field in relevant_fields)
+        
+        if not needs_update:
+            return super().write(vals)
+        
+        # Store current values for comparison - PROCESAR TODAS LAS OPORTUNIDADES
+        current_values = {}
+        for record in self:
+            current_values[record.id] = {
+                'name': record.name,
+                'partner_id': record.partner_id.id if record.partner_id else None,
+                'create_date': record.create_date,
+                'company_id': record.company_id.id if record.company_id else None,
+                'google_drive_folder_name': record.google_drive_folder_name or ''
+            }
+        
+        # Execute the write first
+        result = super().write(vals)
+        
+        # Now process Google Drive updates with updated values - PROCESAR TODAS
+        for record in self:
+            record._process_google_drive_updates_immediate(vals, current_values[record.id])
+        
+        return result
+
+    def _process_google_drive_updates(self, vals):
+        """Process Google Drive updates for a single record"""
+        try:
+            # Check if we need to create folder (stage-based creation)
+            if 'stage_id' in vals and not self.google_drive_folder_id:
+                if not self._validate_folder_creation_prerequisites():
+                    return
+            
+            # If we have a folder, verify and update structure if needed
+            if self.google_drive_folder_id:
+                self._verify_and_update_folder_structure(vals)
+                
+        except Exception as e:
+            _logger.error(f"Error processing Google Drive updates for record {self.id}: {str(e)}")
+            self.message_post(
+                body=_("❌ Error updating Google Drive: %s") % str(e),
+                message_type='comment'
+            )
+
+    def _process_google_drive_updates_immediate(self, vals, old_values):
+        """Process Google Drive updates immediately after write"""
+        try:
+            _logger.info(f"=== INICIO _process_google_drive_updates_immediate para oportunidad {self.id} ===")
+            _logger.info(f"Vals recibidos: {vals}")
+            _logger.info(f"Google Drive Folder ID actual: {self.google_drive_folder_id}")
+            
+            # PASO 1: Verificar si necesitamos crear carpeta (stage-based creation)
+            should_create_folder = False
+            
+            # Caso 1: Cambió stage_id y no tiene carpeta
+            if 'stage_id' in vals and not self.google_drive_folder_id:
+                should_create_folder = True
+                _logger.info(f"CASO 1: Stage changed for opportunity {self.id}. Checking if should create folder.")
+            
+            # Caso 2: Ya está en el stage correcto, no tiene carpeta, y se actualizó cualquier campo
+            elif not self.google_drive_folder_id:
+                # Verificar si está en el stage correcto para crear automáticamente
+                company = self.company_id
+                _logger.info(f"CASO 2: Checking if opportunity {self.id} is in correct stage for auto-creation")
+                _logger.info(f"Company Google Drive enabled: {company.google_drive_crm_enabled}")
+                _logger.info(f"Company stage configured: {company.google_drive_crm_stage_id.name if company.google_drive_crm_stage_id else 'None'}")
+                _logger.info(f"Opportunity stage: {self.stage_id.name}")
+                
+                if (company.google_drive_crm_enabled and 
+                    company.google_drive_crm_stage_id and
+                    self.stage_id.id == company.google_drive_crm_stage_id.id):
+                    should_create_folder = True
+                    _logger.info(f"CASO 2: Opportunity {self.id} is in correct stage but has no folder. Will create.")
+                else:
+                    _logger.info(f"CASO 2: Opportunity {self.id} is NOT in correct stage for auto-creation")
+            
+            _logger.info(f"¿Debería crear carpeta?: {should_create_folder}")
+            
+            # Crear carpeta si es necesario
+            if should_create_folder:
+                _logger.info(f"Intentando crear carpeta para oportunidad {self.id}")
+                
+                if self._validate_folder_creation_prerequisites():
+                    _logger.info(f"Prerrequisitos válidos. Creando Google Drive folder para opportunity {self.id}")
+                    try:
+                        self._create_google_drive_folder_structure()
+                        _logger.info(f"Carpeta creada exitosamente para oportunidad {self.id}")
+                        
+                        # Store the initial structure and update URL
+                        if self._store_initial_structure_and_update_url():
+                            self.message_post(
+                                body=_("✅ Google Drive folder created automatically"),
+                                message_type='comment'
+                            )
+                    except Exception as e:
+                        _logger.error(f"ERROR creando carpeta para oportunidad {self.id}: {str(e)}")
+                        raise
+                else:
+                    _logger.info(f"Prerrequisitos no cumplidos para oportunidad {self.id}. Skipping folder creation.")
+            
+            # PASO 2: Si ya tiene carpeta, verificar y actualizar estructura si es necesario
+            if self.google_drive_folder_id:
+                _logger.info(f"Oportunidad {self.id} ya tiene carpeta. Verificando estructura.")
+                self._verify_and_update_folder_structure_immediate(vals, old_values)
+            
+            _logger.info(f"=== FIN _process_google_drive_updates_immediate para oportunidad {self.id} ===")
+                
+        except Exception as e:
+            _logger.error(f"Error processing Google Drive updates for record {self.id}: {str(e)}")
+            self.message_post(
+                body=_("❌ Error updating Google Drive: %s") % str(e),
+                message_type='comment'
+            )
+
+    def _verify_and_update_folder_structure_immediate(self, vals, old_values):
+        """Verificar y actualizar estructura de Google Drive - SECUENCIAL"""
+        try:
+            _logger.info(f"Processing Google Drive updates for opportunity {self.id}")
+            
+            # PASO 1: Si cambia company_id y tiene folder_id → Mover
+            if 'company_id' in vals and self.google_drive_folder_id:
+                new_company_id = vals['company_id']
+                if new_company_id != old_values.get('company_id'):
+                    _logger.info(f"Company changed from {old_values.get('company_id')} to {new_company_id}. Moving folder.")
+                    self._move_folder_to_new_company(new_company_id)
+            
+            # PASO 2: Si cambia otro campo → Verificar string de estructura
+            relevant_fields = ['name', 'partner_id', 'create_date']
+            if any(field in vals for field in relevant_fields):
+                expected_structure = self._build_structure_string(self._get_folder_name_components())
+                current_structure = old_values.get('google_drive_folder_name', '')
+                
+                _logger.info(f"Structure comparison: Current='{current_structure}' vs Expected='{expected_structure}'")
+                
+                if expected_structure != current_structure:
+                    _logger.info(f"Structure changed. Renaming folder structure.")
+                    self._rename_entire_folder_structure_from_components(self._get_folder_name_components())
+            
+            # Actualizar estructura local al final
+            expected_structure = self._build_structure_string(self._get_folder_name_components())
+            self.with_context(skip_google_drive_update=True).write({
+                'google_drive_folder_name': expected_structure
+            })
+            
+            self.message_post(
+                body=_("✅ Google Drive folder structure updated immediately"),
+                message_type='comment'
+            )
+                
+        except Exception as e:
+            _logger.error(f"Error verifying folder structure: {str(e)}")
+            raise
+
+    def _verify_and_update_folder_structure(self, vals):
+        """Verify current structure vs expected and update if needed"""
+        try:
+            # Get expected structure components
+            expected_components = self._get_folder_name_components()
+            
+            # Build expected structure string
+            expected_structure = self._build_structure_string(expected_components)
+            
+            # Get current structure from stored field
+            current_structure = self.google_drive_folder_name or ''
+            
+            _logger.info(f"Structure comparison for opportunity {self.id}:")
+            _logger.info(f"Current: '{current_structure}'")
+            _logger.info(f"Expected: '{expected_structure}'")
+            
+            # Compare structures
+            if current_structure != expected_structure:
+                _logger.info(f"Structure mismatch detected. Updating Google Drive...")
+                
+                # Determine what type of change occurred
+                if 'company_id' in vals:
+                    self._handle_company_change(vals['company_id'])
+                else:
+                    # For other changes, rename the structure
+                    self._rename_entire_folder_structure_from_components(expected_components)
+                
+                # Update the stored structure
+                self.with_context(skip_google_drive_update=True).write({
+                    'google_drive_folder_name': expected_structure
+                })
+                
+                self.message_post(
+                    body=_("✅ Google Drive folder structure updated successfully"),
+                    message_type='comment'
+                )
+            else:
+                _logger.info(f"Structure is up to date. No changes needed.")
+                
+        except Exception as e:
+            _logger.error(f"Error verifying folder structure: {str(e)}")
+            raise
+
+    def _build_structure_string(self, components):
+        """Build a string representation of the folder structure"""
+        return f"{components['primary_name']}/{components['year']}/{components['opportunity_name']}"
+
+    def _rename_entire_folder_structure_from_components(self, expected_components):
+        """Rename entire folder structure based on expected components"""
+        try:
+            access_token = self._get_google_drive_access_token()
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+            
+            # Get current structure from Google Drive
+            current_structure = self._analyze_complete_folder_structure(headers)
+            
+            if not current_structure:
+                raise UserError(_('Could not analyze current folder structure'))
+            
+            # Build current components from actual structure
+            current_components = {
+                'primary_name': current_structure.get('primary_folder', {}).get('name', ''),
+                'year': current_structure.get('year_folder', {}).get('name', ''),
+                'opportunity_name': current_structure.get('opportunity_folder', {}).get('name', '')
+            }
+            
+            # Rename the structure
+            self._rename_entire_folder_structure(current_components, expected_components)
+            
+        except Exception as e:
+            _logger.error(f"Error renaming folder structure: {str(e)}")
+            raise
+
+    def _move_folder_to_new_company(self, new_company_id):
+        """Mover folder a nueva empresa - SIMPLE"""
+        if not self.google_drive_folder_id:
+            return
+        
+        try:
+            # Obtener nueva empresa
+            new_company = self.env['res.company'].browse(new_company_id)
+            new_root_folder_id = new_company.google_drive_crm_folder_id
+            
+            if not new_root_folder_id:
+                self.message_post(
+                    body=_("⚠️ No se puede mover: Nueva empresa no tiene Google Drive configurado."),
+                    message_type='comment'
+                )
+                return
+            
+            _logger.info(f"Moviendo folder de {self.company_id.name} a {new_company.name}")
+            
+            # Obtener access token
+            access_token = self._get_google_drive_access_token()
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+            
+            # Mover el folder primario al nuevo root
+            self._move_folder_to_new_parent(headers, new_root_folder_id)
+            
+            self.message_post(
+                body=_("✅ Folder movido a nueva empresa: %s") % new_company.name,
+                message_type='comment'
+            )
+            
+        except Exception as e:
+            _logger.error(f"Error moviendo folder: {str(e)}")
+            raise
+
+    def _handle_structural_changes(self, vals):
+        """Handle structural changes (partner_id, create_date) - rename entire structure"""
+        if not self.google_drive_folder_id:
+            return
+        
+        try:
+            # Get current and new components
+            current_components = self._get_folder_name_components()
+            
+            # Temporarily update the record to get new components
+            temp_record = self.with_context(skip_google_drive_update=True)
+            temp_vals = {}
+            if 'partner_id' in vals:
+                temp_vals['partner_id'] = vals['partner_id']
+            if 'create_date' in vals:
+                temp_vals['create_date'] = vals['create_date']
+            
+            if temp_vals:
+                temp_record.write(temp_vals)
+                new_components = temp_record._get_folder_name_components()
+                
+                # Check if any component changed
+                components_changed = (
+                    current_components['primary_name'] != new_components['primary_name'] or
+                    current_components['year'] != new_components['year']
+                )
+                
+                if components_changed:
+                    _logger.info(f"Structural changes detected. Renaming folder structure.")
+                    self._rename_entire_folder_structure(current_components, new_components)
+                else:
+                    _logger.info(f"No structural changes detected. Skipping rename.")
+            
+        except Exception as e:
+            _logger.error(f"Error handling structural changes: {str(e)}")
+            raise
+
+    def _handle_name_change(self, new_name):
+        """Handle simple name change - only rename opportunity folder"""
+        if not self.google_drive_folder_id:
+            return
+        
+        try:
+            sanitized_new_name = self._sanitize_folder_name(new_name)
+            self._rename_google_drive_folder(sanitized_new_name)
+        except Exception as e:
+            _logger.error(f"Error handling name change: {str(e)}")
+            raise
+
+    def _move_folder_to_new_parent(self, headers, new_parent_id):
+        """Move entire folder structure to new parent in Google Drive"""
+        try:
+            # Get current folder info and navigate up to find the primary folder (company/contact)
+            current_folder_id = self.google_drive_folder_id
+            
+            # Navigate up the hierarchy to find the primary folder
+            primary_folder_id = self._find_primary_folder_id(headers, current_folder_id)
+            
+            if not primary_folder_id:
+                raise UserError(_('Could not find primary folder in hierarchy'))
+            
+            # Get current parent of primary folder
+            response = requests.get(
+                f'https://www.googleapis.com/drive/v3/files/{primary_folder_id}?fields=parents',
+                headers=headers,
+                timeout=30
+            )
+            
+            if response.status_code != 200:
+                raise UserError(_('Failed to get primary folder information'))
+            
+            current_data = response.json()
+            current_parents = current_data.get('parents', [])
+            
+            if not current_parents:
+                raise UserError(_('Primary folder has no parent'))
+            
+            current_parent_id = current_parents[0]
+            
+            # Move the entire structure by moving the primary folder
+            move_response = requests.patch(
+                f'https://www.googleapis.com/drive/v3/files/{primary_folder_id}?addParents={new_parent_id}&removeParents={current_parent_id}',
+                headers=headers,
+                timeout=30
+            )
+            
+            if move_response.status_code != 200:
+                raise UserError(_('Failed to move folder structure to new parent'))
+            
+            _logger.info(f"Successfully moved entire folder structure from {current_parent_id} to {new_parent_id}")
+            
+        except Exception as e:
+            _logger.error(f"Error moving folder structure: {str(e)}")
+            raise
+
+    def _find_primary_folder_id(self, headers, start_folder_id):
+        """Find the primary folder (company/contact level) in the hierarchy"""
+        try:
+            current_id = start_folder_id
+            
+            # Navigate up to 3 levels to find the primary folder
+            for _ in range(3):
+                response = requests.get(
+                    f'https://www.googleapis.com/drive/v3/files/{current_id}?fields=name,parents',
+                    headers=headers,
+                    timeout=30
+                )
+                
+                if response.status_code != 200:
+                    break
+                
+                folder_data = response.json()
+                folder_name = folder_data.get('name', '')
+                parent_ids = folder_data.get('parents', [])
+                
+                # Check if this is the primary folder (not year, not opportunity)
+                # Primary folder is typically the company/contact name
+                if not parent_ids:
+                    break
+                
+                # If this folder's name looks like a year (4 digits), continue up
+                if folder_name.isdigit() and len(folder_name) == 4:
+                    current_id = parent_ids[0]
+                    continue
+                
+                # If this folder's name contains opportunity ID pattern, continue up
+                if ' - ' in folder_name and folder_name.split(' - ')[0].isdigit():
+                    current_id = parent_ids[0]
+                    continue
+                
+                # This should be the primary folder
+                return current_id
+            
+            return None
+            
+        except Exception as e:
+            _logger.error(f"Error finding primary folder: {str(e)}")
+            return None
+
+    def action_open_google_drive_folder(self):
+        """Open Google Drive folder for this opportunity"""
+        self.ensure_one()
+        
+        if not self.google_drive_folder_id:
+            raise UserError(_('No Google Drive folder configured for this opportunity'))
+        
+        folder_url = f"https://drive.google.com/drive/folders/{self.google_drive_folder_id}"
+        
+        return {
+            'type': 'ir.actions.act_url',
+            'url': folder_url,
+            'target': 'new',
+        }
+
+    def action_create_google_drive_folder(self):
+        """Create Google Drive folder structure for this opportunity"""
+        self.ensure_one()
+        
+        if self.google_drive_folder_id:
+            raise UserError(_('Google Drive folder already exists for this opportunity'))
+        
+        # Check if company has Google Drive folder configured
+        root_folder_id = self._get_company_root_folder_id()
+        if not root_folder_id:
+            raise UserError(_('Google Drive CRM folder is not configured for this company. Please configure it in company settings.'))
+        
+        try:
+            folder_structure = self._create_google_drive_folder_structure()
+            
+            # Store the initial structure and update URL
+            self._store_initial_structure_and_update_url()
+            
+            return {
+                'type': 'ir.actions.client',
+                'tag': 'display_notification',
+                'params': {
+                    'title': _('Success'),
+                    'message': _('Google Drive folder structure created successfully!'),
+                    'type': 'success',
+                    'sticky': False,
+                }
+            }
+        except Exception as e:
+            raise UserError(_('Failed to create Google Drive folder structure: %s') % str(e))
+
+    def action_upload_to_google_drive(self):
+        """Upload documents to Google Drive"""
+        self.ensure_one()
+        
+        if not self.google_drive_folder_id:
+            raise UserError(_('Please create a Google Drive folder for this opportunity first'))
+        
+        try:
+            # TODO: Implement Google Drive API call to upload documents
+            # For now, just show a message
+            return {
+                'type': 'ir.actions.client',
+                'tag': 'display_notification',
+                'params': {
+                    'title': _('Info'),
+                    'message': _('Document upload to Google Drive will be implemented soon.'),
+                    'type': 'info',
+                    'sticky': False,
+                }
+            }
+            
+        except Exception as e:
+            raise UserError(_('Failed to upload to Google Drive: %s') % str(e))
+
+    def action_recreate_google_drive_structure(self):
+        """Manually rename the Google Drive folder structure"""
+        self.ensure_one()
+        
+        if not self.google_drive_folder_id:
+            raise UserError(_('No Google Drive folder exists for this opportunity. Please create one first.'))
+        
+        # Check if company has Google Drive folder configured
+        root_folder_id = self._get_company_root_folder_id()
+        if not root_folder_id:
+            raise UserError(_('Google Drive CRM folder is not configured for this company. Please configure it in company settings.'))
+        
+        try:
+            # Get expected components
+            expected_components = self._get_folder_name_components()
+            
+            # Get current folder name from Google Drive
+            access_token = self._get_google_drive_access_token()
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+            
+            response = requests.get(
+                f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}?fields=name',
+                headers=headers,
+                timeout=30
+            )
+            
+            if response.status_code != 200:
+                raise UserError(_('Failed to get current folder information from Google Drive'))
+            
+            current_folder_data = response.json()
+            current_folder_name = current_folder_data.get('name', '')
+            
+            _logger.info(f"Current folder name in Google Drive: '{current_folder_name}'")
+            _logger.info(f"Expected folder name: '{expected_components['opportunity_name']}'")
+            
+            # Create old components with current Google Drive name
+            old_components = expected_components.copy()
+            old_components['opportunity_name'] = current_folder_name
+            
+            # Rename the structure
+            self._rename_entire_folder_structure(old_components, expected_components)
+            
+            # Update the stored structure
+            expected_structure = self._build_structure_string(expected_components)
+            self.with_context(skip_google_drive_update=True).write({
+                'google_drive_folder_name': expected_structure
+            })
+            
+            return {
+                'type': 'ir.actions.client',
+                'tag': 'display_notification',
+                'params': {
+                    'title': _('Success'),
+                    'message': _('Google Drive folder structure renamed successfully!<br/>'
+                                'Folder ID: %s<br/>'
+                                'Old name: %s<br/>'
+                                'New name: %s') % (self.google_drive_folder_id, current_folder_name, expected_components['opportunity_name']),
+                    'type': 'success',
+                    'sticky': False,
+                }
+            }
+        except Exception as e:
+            _logger.error(f"Failed to rename Google Drive folder structure: {str(e)}")
+            raise UserError(_('Failed to rename Google Drive folder structure: %s') % str(e))
+
+    def action_analyze_folder_structure(self):
+        """Analyze current vs expected folder structure"""
+        self.ensure_one()
+        
+        if not self.google_drive_folder_id:
+            raise UserError(_('No Google Drive folder exists for this opportunity. Please create one first.'))
+        
+        try:
+            # Get expected components
+            expected_components = self._get_folder_name_components()
+            
+            # Get current folder information
+            access_token = self._get_google_drive_access_token()
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+            
+            # Analyze current structure
+            current_structure = self._analyze_complete_folder_structure(headers)
+            
+            # Compare structures
+            analysis = self._compare_folder_structures(expected_components, current_structure, headers)
+            
+            return {
+                'type': 'ir.actions.client',
+                'tag': 'display_notification',
+                'params': {
+                    'title': _('Folder Structure Analysis'),
+                    'message': analysis,
+                    'type': 'info',
+                    'sticky': True,
+                }
+            }
+        except Exception as e:
+            raise UserError(_('Failed to analyze folder structure: %s') % str(e))
+
+    def _analyze_current_folder_structure(self, headers):
+        """Analyze the current folder structure in Google Drive"""
+        current_folder_id = self.google_drive_folder_id
+        
+        # Get current folder info
+        response = requests.get(
+            f'https://www.googleapis.com/drive/v3/files/{current_folder_id}?fields=name,parents',
+            headers=headers,
+            timeout=30
+        )
+        
+        if response.status_code != 200:
+            return None
+        
+        folder_data = response.json()
+        current_name = folder_data.get('name', '')
+        parent_ids = folder_data.get('parents', [])
+        
+        # Navigate up the hierarchy
+        structure = {
+            'opportunity_folder': {
+                'id': current_folder_id,
+                'name': current_name
+            }
+        }
+        
+        if parent_ids:
+            # Get year folder
+            year_response = requests.get(
+                f'https://www.googleapis.com/drive/v3/files/{parent_ids[0]}?fields=name,parents',
+                headers=headers,
+                timeout=30
+            )
+            
+            if year_response.status_code == 200:
+                year_data = year_response.json()
+                structure['year_folder'] = {
+                    'id': parent_ids[0],
+                    'name': year_data.get('name', '')
+                }
+                
+                year_parent_ids = year_data.get('parents', [])
+                if year_parent_ids:
+                    # Get primary folder (company/contact)
+                    primary_response = requests.get(
+                        f'https://www.googleapis.com/drive/v3/files/{year_parent_ids[0]}?fields=name',
+                        headers=headers,
+                        timeout=30
+                    )
+                    
+                    if primary_response.status_code == 200:
+                        primary_data = primary_response.json()
+                        structure['primary_folder'] = {
+                            'id': year_parent_ids[0],
+                            'name': primary_data.get('name', '')
+                        }
+        
+        return structure
+
+    def _analyze_complete_folder_structure(self, headers):
+        """Analyze the complete folder structure from root to opportunity"""
+        current_folder_id = self.google_drive_folder_id
+        
+        # Get current folder info
+        response = requests.get(
+            f'https://www.googleapis.com/drive/v3/files/{current_folder_id}?fields=name,parents',
+            headers=headers,
+            timeout=30
+        )
+        
+        if response.status_code != 200:
+            return None
+        
+        folder_data = response.json()
+        current_name = folder_data.get('name', '')
+        parent_ids = folder_data.get('parents', [])
+        
+        # Build complete structure
+        complete_structure = {
+            'opportunity_folder': {
+                'id': current_folder_id,
+                'name': current_name,
+                'level': 3
+            }
+        }
+        
+        current_id = current_folder_id
+        level = 3  # Opportunity level
+        
+        # Navigate up the hierarchy
+        for _ in range(5):  # Max 5 levels up
+            if not parent_ids:
+                break
+                
+            parent_id = parent_ids[0]
+            
+            # Get parent folder info
+            parent_response = requests.get(
+                f'https://www.googleapis.com/drive/v3/files/{parent_id}?fields=name,parents',
+                headers=headers,
+                timeout=30
+            )
+            
+            if parent_response.status_code != 200:
+                break
+            
+            parent_data = parent_response.json()
+            parent_name = parent_data.get('name', '')
+            parent_ids = parent_data.get('parents', [])
+            
+            level -= 1
+            
+            if level == 2:  # Year level
+                complete_structure['year_folder'] = {
+                    'id': parent_id,
+                    'name': parent_name,
+                    'level': level
+                }
+            elif level == 1:  # Primary level (company/contact)
+                complete_structure['primary_folder'] = {
+                    'id': parent_id,
+                    'name': parent_name,
+                    'level': level
+                }
+            elif level == 0:  # Root level
+                complete_structure['root_folder'] = {
+                    'id': parent_id,
+                    'name': parent_name,
+                    'level': level
+                }
+            
+            current_id = parent_id
+        
+        return complete_structure
+
+    def _compare_folder_structures(self, expected_components, current_structure, headers):
+        """Compare expected vs current folder structure"""
+        if not current_structure:
+            return _('❌ Could not analyze current folder structure')
+        
+        analysis = f"<strong>📁 Complete Folder Structure Analysis</strong><br/><br/>"
+        
+        # Expected structure
+        analysis += f"<strong>Expected Structure:</strong><br/>"
+        analysis += f"📁 [Root Folder] (MC Team)<br/>"
+        analysis += f"└── 📁 {expected_components['primary_name']} (Company/Contact)<br/>"
+        analysis += f"    └── 📁 {expected_components['year']} (Year)<br/>"
+        analysis += f"        └── 📁 {expected_components['opportunity_name']} (Opportunity)<br/>"
+        analysis += f"            ├── 📁 Meets<br/>"
+        analysis += f"            └── 📁 Archivos cliente<br/><br/>"
+        
+        # Current structure
+        analysis += f"<strong>Current Structure in Google Drive:</strong><br/>"
+        
+        # Root folder
+        if 'root_folder' in current_structure:
+            root_name = current_structure['root_folder']['name']
+            analysis += f"📁 {root_name} (Root)<br/>"
+        else:
+            analysis += f"📁 [Unknown Root]<br/>"
+        
+        # Primary folder
+        if 'primary_folder' in current_structure:
+            primary_name = current_structure['primary_folder']['name']
+            analysis += f"└── 📁 {primary_name}"
+            if primary_name != expected_components['primary_name']:
+                analysis += f" ❌ (Expected: {expected_components['primary_name']})"
+            else:
+                analysis += " ✅"
+            analysis += "<br/>"
+        else:
+            analysis += f"└── 📁 [Missing Primary Folder] ❌<br/>"
+        
+        # Year folder
+        if 'year_folder' in current_structure:
+            year_name = current_structure['year_folder']['name']
+            analysis += f"    └── 📁 {year_name}"
+            if year_name != expected_components['year']:
+                analysis += f" ❌ (Expected: {expected_components['year']})"
+            else:
+                analysis += " ✅"
+            analysis += "<br/>"
+        else:
+            analysis += f"    └── 📁 [Missing Year Folder] ❌<br/>"
+        
+        # Opportunity folder
+        if 'opportunity_folder' in current_structure:
+            opp_name = current_structure['opportunity_folder']['name']
+            analysis += f"        └── 📁 {opp_name}"
+            if opp_name != expected_components['opportunity_name']:
+                analysis += f" ❌ (Expected: {expected_components['opportunity_name']})"
+            else:
+                analysis += " ✅"
+            analysis += "<br/>"
+        else:
+            analysis += f"        └── 📁 [Missing Opportunity Folder] ❌<br/>"
+        
+        # Check subfolders
+        if 'opportunity_folder' in current_structure:
+            opp_id = current_structure['opportunity_folder']['id']
+            subfolders = self._get_subfolders(headers, opp_id)
+            if subfolders:
+                analysis += f"            ├── 📁 Meets ✅<br/>"
+                analysis += f"            └── 📁 Archivos cliente ✅<br/>"
+            else:
+                analysis += f"            ├── 📁 Meets ❌ (Missing)<br/>"
+                analysis += f"            └── 📁 Archivos cliente ❌ (Missing)<br/>"
+        
+        # Summary
+        analysis += f"<br/><strong>Summary:</strong><br/>"
+        correct_count = 0
+        total_count = 0
+        
+        if 'primary_folder' in current_structure:
+            total_count += 1
+            if current_structure['primary_folder']['name'] == expected_components['primary_name']:
+                correct_count += 1
+        
+        if 'year_folder' in current_structure:
+            total_count += 1
+            if current_structure['year_folder']['name'] == expected_components['year']:
+                correct_count += 1
+        
+        if 'opportunity_folder' in current_structure:
+            total_count += 1
+            if current_structure['opportunity_folder']['name'] == expected_components['opportunity_name']:
+                correct_count += 1
+        
+        if total_count == 3 and correct_count == 3:
+            analysis += "✅ Complete structure is correct"
+        else:
+            analysis += f"❌ Structure has issues ({correct_count}/{total_count} correct). Use 'Rename Folder Structure' button to fix."
+        
+        return analysis
+
+    def _get_subfolders(self, headers, parent_id):
+        """Get subfolders of a parent folder"""
+        params = {
+            'q': f"'{parent_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false",
+            'fields': 'files(id,name)',
+            'pageSize': 100
+        }
+        
+        try:
+            response = requests.get(
+                'https://www.googleapis.com/drive/v3/files',
+                headers=headers,
+                params=params,
+                timeout=30
+            )
+            
+            if response.status_code == 200:
+                data = response.json()
+                return data.get('files', [])
+            else:
+                return []
+        except:
+            return []
+
+    def _store_initial_structure_and_update_url(self):
+        """Centralized method to store initial structure and update URL"""
+        if self.google_drive_folder_id:
+            expected_components = self._get_folder_name_components()
+            expected_structure = self._build_structure_string(expected_components)
+            self.with_context(skip_google_drive_update=True).write({
+                'google_drive_folder_name': expected_structure
+            })
+            
+            # Update Google Drive URL if empty
+            self._update_google_drive_url()
+            
+            # Copy URL to configured field if it's empty
+            self._copy_google_drive_url_to_configured_field()
+            
+            _logger.info(f"Estructura inicial almacenada para oportunidad {self.id}")
+            return True
+        else:
+            _logger.error(f"ERROR: _create_google_drive_folder_structure no asignó google_drive_folder_id para oportunidad {self.id}")
+            return False
+
+    def _generate_google_drive_url(self):
+        """Generate Google Drive URL for the opportunity folder"""
+        if self.google_drive_folder_id:
+            return f"https://drive.google.com/drive/folders/{self.google_drive_folder_id}"
+        return False
+
+    def _update_google_drive_url(self):
+        """Update the google_drive_url field if it's empty and we have a folder ID"""
+        if self.google_drive_folder_id and not self.google_drive_url:
+            url = self._generate_google_drive_url()
+            if url:
+                self.with_context(skip_google_drive_update=True).write({
+                    'google_drive_url': url
+                })
+                _logger.info(f"Updated Google Drive URL for opportunity {self.id}: {url}")
+
+    def _extract_folder_id_from_url(self, url):
+        """Extract folder ID from Google Drive URL"""
+        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)
+        
+        return None
+
+    def _get_configured_field_name(self):
+        """Get the field name configured in company settings"""
+        if not self.company_id or not self.company_id.google_drive_crm_field_id:
+            return None
+        return self.company_id.google_drive_crm_field_id.name
+
+    def _get_configured_field_value(self):
+        """Get the value of the configured field"""
+        field_name = self._get_configured_field_name()
+        if not field_name:
+            return None
+        return getattr(self, field_name, None)
+
+    def _set_configured_field_value(self, value):
+        """Set the value of the configured field"""
+        field_name = self._get_configured_field_name()
+        if not field_name:
+            return False
+        self.with_context(skip_google_drive_update=True).write({field_name: value})
+        return True
+
+    def _copy_google_drive_url_to_configured_field(self):
+        """Copy google_drive_url to the configured field if it's empty"""
+        if not self.google_drive_url:
+            return False
+        
+        field_name = self._get_configured_field_name()
+        if not field_name:
+            return False
+        
+        current_value = getattr(self, field_name, None)
+        if not current_value:  # Solo si está vacío
+            self._set_configured_field_value(self.google_drive_url)
+            _logger.info(f"Copied google_drive_url to {field_name} for opportunity {self.id}")
+            return True
+        
+        return False
+
+    def _validate_folder_id_with_google_drive(self, folder_id):
+        """Validate if the folder ID exists and is accessible in Google Drive"""
+        try:
+            access_token = self._get_google_drive_access_token()
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+            
+            # Test access to the specific folder
+            response = requests.get(
+                f'https://www.googleapis.com/drive/v3/files/{folder_id}?fields=id,name,mimeType',
+                headers=headers,
+                timeout=10
+            )
+            
+            if response.status_code == 200:
+                folder_data = response.json()
+                if folder_data.get('mimeType') == 'application/vnd.google-apps.folder':
+                    _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
+                    return True, folder_data.get('name', 'Unknown')
+                else:
+                    _logger.warning(f"❌ ID {folder_id} exists but is not a folder")
+                    return False, "Not a folder"
+            elif response.status_code == 404:
+                _logger.warning(f"❌ Folder ID {folder_id} not found in Google Drive")
+                return False, "Not found"
+            elif response.status_code == 403:
+                _logger.warning(f"❌ Access denied to folder ID {folder_id}")
+                return False, "Access denied"
+            elif response.status_code == 401:
+                _logger.error(f"❌ OAuth token expired or invalid")
+                return False, "Authentication error"
+            else:
+                _logger.warning(f"❌ Google Drive API error: {response.status_code}")
+                return False, f"API error: {response.status_code}"
+                
+        except requests.exceptions.Timeout:
+            _logger.error(f"❌ Timeout validating folder ID {folder_id}")
+            return False, "Timeout"
+        except requests.exceptions.ConnectionError:
+            _logger.error(f"❌ Connection error validating folder ID {folder_id}")
+            return False, "Connection error"
+        except Exception as e:
+            _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
+            return False, str(e)
+
+

+ 246 - 0
m22tc_google_workspace/models/res_company.py

@@ -0,0 +1,246 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models, api, _
+from odoo.exceptions import UserError
+
+
+class ResCompany(models.Model):
+    _inherit = 'res.company'
+
+    google_drive_crm_folder_id = fields.Char(
+        string='Google Drive CRM Folder ID',
+        help='ID del folder en Google Drive para documentos del CRM'
+    )
+    google_drive_crm_folder_name = fields.Char(
+        string='Google Drive CRM Folder Name',
+        help='Nombre del folder en Google Drive para documentos del CRM',
+        readonly=True
+    )
+    google_drive_crm_enabled = fields.Boolean(
+        string='Enable Google Drive CRM Integration',
+        default=False,
+        help='Habilitar integración con Google Drive para CRM'
+    )
+    google_drive_crm_stage_id = fields.Many2one(
+        'crm.stage',
+        string='CRM Stage for Google Drive Folder Creation',
+        help='Etapa del CRM en la que se creará automáticamente la carpeta en Google Drive'
+    )
+    google_drive_crm_field_id = fields.Many2one(
+        'ir.model.fields',
+        string='Google Drive Field',
+        domain=[('model', '=', 'crm.lead')],
+        help='Campo opcional de crm.lead que contiene información de Google Drive'
+    )
+
+    @api.onchange('google_drive_crm_folder_id')
+    def _onchange_google_drive_crm_folder_id(self):
+        """Update folder name when folder ID changes"""
+        if self.google_drive_crm_folder_id:
+            # TODO: Implement Google Drive API call to get folder name
+            # For now, just clear the name
+            self.google_drive_crm_folder_name = False
+
+    def action_test_google_drive_connection(self):
+        """Test Google Drive connection for this company"""
+        self.ensure_one()
+        
+        if not self.google_drive_crm_enabled:
+            raise UserError(_('Google Drive CRM Integration is not enabled for this company'))
+        
+        if not self.google_drive_crm_folder_id:
+            raise UserError(_('Please set a Google Drive CRM Folder ID first'))
+        
+        try:
+            import requests
+            import json
+            
+            # Get Google API credentials from system parameters
+            google_api_enabled = self.env['ir.config_parameter'].sudo().get_param('google_api.enabled', 'False')
+            google_api_client_id = self.env['ir.config_parameter'].sudo().get_param('google_api.client_id', '')
+            google_api_client_secret = self.env['ir.config_parameter'].sudo().get_param('google_api.client_secret', '')
+            
+            if not google_api_enabled or google_api_enabled == 'False':
+                raise UserError(_('Google API Integration is not enabled in system settings'))
+            
+            if not google_api_client_id or not google_api_client_secret:
+                raise UserError(_('Google API credentials are not configured in system settings'))
+            
+            # Get manual OAuth token
+            access_token = self.env['ir.config_parameter'].sudo().get_param('google_api.access_token')
+            if not access_token:
+                raise UserError(_('No OAuth token found. Please connect your Google account in Google API settings first.'))
+            
+            # Validate folder ID format (Google Drive folder IDs are typically 33 characters)
+            if len(self.google_drive_crm_folder_id) < 10 or len(self.google_drive_crm_folder_id) > 50:
+                raise UserError(_('Google Drive Folder ID format appears to be invalid (should be 10-50 characters)'))
+            
+            # Test Google Drive API access with OAuth token
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+            }
+            
+            # Test access to the specific folder
+            drive_api_url = f"https://www.googleapis.com/drive/v3/files/{self.google_drive_crm_folder_id}"
+            drive_response = requests.get(drive_api_url, headers=headers, timeout=10)
+            
+            if drive_response.status_code == 200:
+                folder_data = drive_response.json()
+                folder_name = folder_data.get('name', 'Unknown')
+                return {
+                    'type': 'ir.actions.client',
+                    'tag': 'display_notification',
+                    'params': {
+                        'title': _('Success'),
+                        'message': _('Google Drive connection test successful! Folder "%s" is accessible.') % folder_name,
+                        'type': 'success',
+                        'sticky': False,
+                    }
+                }
+            elif drive_response.status_code == 404:
+                raise UserError(_('Google Drive folder not found. Please verify the Folder ID is correct.'))
+            elif drive_response.status_code == 403:
+                raise UserError(_('Access denied to Google Drive folder. Please check folder permissions.'))
+            elif drive_response.status_code == 401:
+                raise UserError(_('OAuth token expired or invalid. Please reconnect your Google account in Google API settings.'))
+            else:
+                raise UserError(_('Google Drive API test failed. Status: %s') % drive_response.status_code)
+            
+        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(_('Google Drive connection test failed: %s') % str(e))
+
+    def action_open_google_drive_folder(self):
+        """Open Google Drive folder in browser"""
+        self.ensure_one()
+        
+        if not self.google_drive_crm_folder_id:
+            raise UserError(_('No Google Drive CRM folder configured'))
+        
+        folder_url = f"https://drive.google.com/drive/folders/{self.google_drive_crm_folder_id}"
+        
+        return {
+            'type': 'ir.actions.act_url',
+            'url': folder_url,
+            'target': 'new',
+        }
+
+    def action_create_google_drive_folder(self):
+        """Create a folder in Google Drive using manual OAuth flow"""
+        self.ensure_one()
+        
+        if not self.google_drive_crm_enabled:
+            raise UserError(_('Google Drive CRM Integration is not enabled for this company'))
+        
+        # Get manual OAuth token
+        access_token = self.env['ir.config_parameter'].sudo().get_param('google_api.access_token')
+        if not access_token:
+            raise UserError(_('No OAuth token found. Please connect your Google account in Google API settings first.'))
+        
+        try:
+            import requests
+            from datetime import datetime
+            
+            # Test Google Drive API access with OAuth token
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+                'Content-Type': 'application/json'
+            }
+            
+            # Create folder metadata
+            folder_name = f"CRM Folder - {self.name} - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
+            folder_metadata = {
+                'name': folder_name,
+                'mimeType': 'application/vnd.google-apps.folder'
+            }
+            
+            # Add parent folder if specified
+            if self.google_drive_crm_folder_id:
+                folder_metadata['parents'] = [self.google_drive_crm_folder_id]
+            
+            # Create folder
+            response = requests.post(
+                'https://www.googleapis.com/drive/v3/files',
+                headers=headers,
+                json=folder_metadata,
+                timeout=30
+            )
+            
+            if response.status_code == 200:
+                folder_data = response.json()
+                return {
+                    'type': 'ir.actions.client',
+                    'tag': 'display_notification',
+                    'params': {
+                        'title': _('Success'),
+                        'message': _('Google Drive folder created successfully: %s') % folder_data.get('name'),
+                        'type': 'success',
+                        'sticky': False,
+                    }
+                }
+            elif response.status_code == 401:
+                raise UserError(_('OAuth token expired or invalid. Please reconnect your Google account in Google API settings.'))
+            else:
+                raise UserError(_('Failed to create Google Drive folder. Status: %s') % response.status_code)
+                
+        except Exception as e:
+            raise UserError(_('Failed to create Google Drive folder: %s') % str(e))
+
+    def action_list_google_drive_folders(self):
+        """List folders in Google Drive using manual OAuth flow"""
+        self.ensure_one()
+        
+        if not self.google_drive_crm_enabled:
+            raise UserError(_('Google Drive CRM Integration is not enabled for this company'))
+        
+        # Get manual OAuth token
+        access_token = self.env['ir.config_parameter'].sudo().get_param('google_api.access_token')
+        if not access_token:
+            raise UserError(_('No OAuth token found. Please connect your Google account in Google API settings first.'))
+        
+        try:
+            import requests
+            
+            headers = {
+                'Authorization': f'Bearer {access_token}',
+            }
+            
+            # List folders
+            params = {
+                'q': "mimeType='application/vnd.google-apps.folder' and trashed=false",
+                'fields': 'files(id,name,webViewLink)',
+                'pageSize': 10
+            }
+            
+            response = requests.get(
+                'https://www.googleapis.com/drive/v3/files',
+                headers=headers,
+                params=params,
+                timeout=30
+            )
+            
+            if response.status_code == 200:
+                data = response.json()
+                folders = data.get('files', [])
+                
+                return {
+                    'type': 'ir.actions.client',
+                    'tag': 'display_notification',
+                    'params': {
+                        'title': _('Success'),
+                        'message': _('Found %d folders in Google Drive.') % len(folders),
+                        'type': 'success',
+                        'sticky': False,
+                    }
+                }
+            elif response.status_code == 401:
+                raise UserError(_('OAuth token expired or invalid. Please reconnect your Google account in Google API settings.'))
+            else:
+                raise UserError(_('Failed to list Google Drive folders. Status: %s') % response.status_code)
+                
+        except Exception as e:
+            raise UserError(_('Failed to list Google Drive folders: %s') % str(e))

+ 2 - 0
m22tc_google_workspace/security/ir.model.access.csv

@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_res_company_google_workspace,res.company.google.workspace,model_res_company,base.group_system,1,1,1,0

+ 58 - 0
m22tc_google_workspace/static/src/css/google_drive_widget.css

@@ -0,0 +1,58 @@
+/* Google Drive Widget Styles */
+
+.google-drive-widget {
+    display: flex;
+    align-items: center;
+    gap: 0.5rem;
+}
+
+.google-drive-folder-info {
+    background-color: #f8f9fa;
+    border: 1px solid #dee2e6;
+    border-radius: 0.375rem;
+    padding: 1rem;
+    margin-bottom: 1rem;
+}
+
+.google-drive-folder-info h5 {
+    color: #495057;
+    margin-bottom: 0.5rem;
+}
+
+.google-drive-documents-count {
+    display: inline-flex;
+    align-items: center;
+    gap: 0.25rem;
+    background-color: #e7f3ff;
+    color: #0056b3;
+    padding: 0.25rem 0.5rem;
+    border-radius: 0.25rem;
+    font-size: 0.875rem;
+    font-weight: 500;
+}
+
+.google-drive-actions {
+    display: flex;
+    gap: 0.5rem;
+    margin-top: 1rem;
+}
+
+.google-drive-status {
+    display: inline-block;
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    margin-right: 0.5rem;
+}
+
+.google-drive-status.connected {
+    background-color: #28a745;
+}
+
+.google-drive-status.disconnected {
+    background-color: #dc3545;
+}
+
+.google-drive-status.pending {
+    background-color: #ffc107;
+}

+ 64 - 0
m22tc_google_workspace/static/src/js/google_drive_widget.js

@@ -0,0 +1,64 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { Component, onWillStart } from "@odoo/owl";
+
+class GoogleDriveWidget extends Component {
+    setup() {
+        this.notification = useService("notification");
+        this.rpc = useService("rpc");
+        
+        onWillStart(async () => {
+            // Initialize Google Drive widget
+        });
+    }
+
+    async openGoogleDriveFolder() {
+        try {
+            const result = await this.rpc("/m22tc_google_workspace/open_folder", {
+                record_id: this.props.record.resId,
+            });
+            
+            if (result.success) {
+                window.open(result.url, "_blank");
+            } else {
+                this.notification.add(result.message, {
+                    type: "danger",
+                });
+            }
+        } catch (error) {
+            this.notification.add("Failed to open Google Drive folder", {
+                type: "danger",
+            });
+        }
+    }
+
+    async createGoogleDriveFolder() {
+        try {
+            const result = await this.rpc("/m22tc_google_workspace/create_folder", {
+                record_id: this.props.record.resId,
+            });
+            
+            if (result.success) {
+                this.notification.add("Google Drive folder created successfully", {
+                    type: "success",
+                });
+                // Refresh the view
+                this.props.record.load();
+            } else {
+                this.notification.add(result.message, {
+                    type: "danger",
+                });
+            }
+        } catch (error) {
+            this.notification.add("Failed to create Google Drive folder", {
+                type: "danger",
+            });
+        }
+    }
+}
+
+GoogleDriveWidget.template = "m22tc_google_workspace.GoogleDriveWidget";
+
+registry.category("actions").add("google_drive_widget", GoogleDriveWidget);

+ 99 - 0
m22tc_google_workspace/views/crm_lead_views.xml

@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <!-- CRM Lead Form View -->
+    <record id="crm_lead_view_form_inherit_google_workspace" model="ir.ui.view">
+        <field name="name">crm.lead.form.inherit.google.workspace</field>
+        <field name="model">crm.lead</field>
+        <field name="inherit_id" ref="crm.crm_lead_view_form"/>
+        <field name="arch" type="xml">
+            <xpath expr="//notebook" position="inside">
+                <page string="Google Drive" name="google_drive" invisible="not company_id.google_drive_crm_enabled">
+                    <group>
+                        <group string="Google Drive Integration">
+                            <field name="google_drive_folder_id" readonly="1"/>
+                            <field name="google_drive_folder_name" readonly="1"/>
+                            <field name="google_drive_url" readonly="1" widget="url"/>
+                            <field name="google_drive_documents_count"/>
+                        </group>
+
+                    </group>
+                    <group>
+                        <button name="action_create_google_drive_folder" 
+                                string="Create Google Drive Folder Structure" 
+                                type="object" 
+                                class="btn btn-primary"
+                                invisible="google_drive_folder_id"/>
+                        <button name="action_open_google_drive_folder" 
+                                string="Open Google Drive Folder" 
+                                type="object" 
+                                class="btn btn-secondary"
+                                invisible="not google_drive_folder_id"/>
+                        <button name="action_recreate_google_drive_structure" 
+                                string="Rename Folder Structure" 
+                                type="object" 
+                                class="btn btn-warning"
+                                invisible="not google_drive_folder_id"/>
+                        <button name="action_analyze_folder_structure" 
+                                string="Analyze Structure" 
+                                type="object" 
+                                class="btn btn-secondary"
+                                invisible="not google_drive_folder_id"/>
+                        <button name="action_upload_to_google_drive" 
+                                string="Upload Documents" 
+                                type="object" 
+                                class="btn btn-info"
+                                invisible="not google_drive_folder_id"/>
+                    </group>
+                    <div class="alert alert-info" role="alert">
+                        <strong>Google Drive Integration:</strong>
+                        <ul class="mb-0 mt-2">
+                            <li>Estructura automática: Empresa/Contacto → Año → Oportunidad → [Meets/Archivos cliente]</li>
+                            <li>Las carpetas se crean automáticamente al cambiar a la etapa configurada</li>
+                            <li><strong>Requisito:</strong> La oportunidad debe tener un contacto asignado</li>
+                            <li><strong>Prioridad:</strong> Nombre de empresa (si existe) → Nombre de contacto</li>
+                            <li><strong>Actualización automática:</strong> Al modificar la oportunidad, se revisa toda la estructura</li>
+                            <li><strong>Renombrado automático:</strong> Si cambia el nombre de empresa, contacto o año, se renombran las carpetas existentes</li>
+                            <li>Los folders se renombran automáticamente si cambia el nombre de la oportunidad</li>
+                            <li>Los folders se renombran automáticamente si cambia el contacto o su empresa</li>
+                            <li>Los folders se mueven automáticamente si cambia la empresa</li>
+                            <li><strong>Botón "Rename Folder Structure":</strong> Para renombrar manualmente la estructura</li>
+                            <li><strong>Seguridad:</strong> Las carpetas nunca se eliminan automáticamente</li>
+                        </ul>
+                    </div>
+                </page>
+            </xpath>
+        </field>
+    </record>
+
+
+
+
+
+    <!-- CRM Lead Tree View for Opportunities -->
+    <record id="crm_lead_view_tree_opportunity_inherit_google_workspace" model="ir.ui.view">
+        <field name="name">crm.lead.tree.opportunity.inherit.google.workspace</field>
+        <field name="model">crm.lead</field>
+        <field name="inherit_id" ref="crm.crm_case_tree_view_oppor"/>
+        <field name="arch" type="xml">
+            <xpath expr="//field[@name='expected_revenue']" position="after">
+                <field name="google_drive_documents_count" 
+                       widget="integer" 
+                       invisible="not company_id.google_drive_crm_enabled"/>
+            </xpath>
+        </field>
+    </record>
+
+    <!-- CRM Lead Tree View for Leads -->
+    <record id="crm_lead_view_tree_lead_inherit_google_workspace" model="ir.ui.view">
+        <field name="name">crm.lead.tree.lead.inherit.google.workspace</field>
+        <field name="model">crm.lead</field>
+        <field name="inherit_id" ref="crm.crm_case_tree_view_leads"/>
+        <field name="arch" type="xml">
+            <xpath expr="//field[@name='email_from']" position="after">
+                <field name="google_drive_documents_count" 
+                       widget="integer" 
+                       invisible="not company_id.google_drive_crm_enabled"/>
+            </xpath>
+        </field>
+    </record>
+</odoo>

+ 61 - 0
m22tc_google_workspace/views/res_company_views.xml

@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <record id="view_company_form_inherit_google_workspace" model="ir.ui.view">
+        <field name="name">res.company.form.inherit.google.workspace</field>
+        <field name="model">res.company</field>
+        <field name="inherit_id" ref="base.view_company_form"/>
+        <field name="arch" type="xml">
+            <xpath expr="//notebook" position="inside">
+                <page string="Google Workspace" name="google_workspace">
+                    <group>
+                        <group string="Google Drive CRM Integration">
+                            <field name="google_drive_crm_enabled"/>
+                            <field name="google_drive_crm_folder_id" 
+                                   invisible="not google_drive_crm_enabled"/>
+                            <field name="google_drive_crm_folder_name" 
+                                   invisible="not google_drive_crm_enabled"/>
+                            <field name="google_drive_crm_stage_id" 
+                                   invisible="not google_drive_crm_enabled"
+                                   options="{'no_create': True, 'no_open': True}"/>
+                            <field name="google_drive_crm_field_id" 
+                                   invisible="not google_drive_crm_enabled"
+                                   options="{'no_create': True, 'no_open': True}"/>
+                        </group>
+                    </group>
+                    <group invisible="not google_drive_crm_enabled">
+                        <button name="action_test_google_drive_connection" 
+                                string="Test Google Drive Connection" 
+                                type="object" 
+                                class="btn btn-primary me-2"/>
+                        <button name="action_open_google_drive_folder" 
+                                string="Open Google Drive Folder" 
+                                type="object" 
+                                class="btn btn-secondary me-2"
+                                invisible="not google_drive_crm_folder_id"/>
+                        <button name="action_create_google_drive_folder" 
+                                string="Create Test Folder" 
+                                type="object" 
+                                class="btn btn-secondary me-2"/>
+                        <button name="action_list_google_drive_folders" 
+                                string="List Folders" 
+                                type="object" 
+                                class="btn btn-secondary"/>
+                    </group>
+                    <div class="alert alert-info" role="alert" invisible="not google_drive_crm_enabled">
+                        <strong>Google Drive CRM Configuration:</strong>
+                        <ul class="mb-0 mt-2">
+                            <li>Configure el folder principal de Google Drive para documentos del CRM</li>
+                            <li>Seleccione la etapa del CRM en la que se crearán automáticamente las carpetas</li>
+                            <li>Estructura automática: Empresa/Contacto → Año → Oportunidad → [Meets/Archivos cliente]</li>
+                            <li><strong>Requisito:</strong> Las oportunidades deben tener contacto asignado</li>
+                            <li><strong>Prioridad:</strong> Nombre de empresa (si existe) → Nombre de contacto</li>
+                            <li><strong>Actualización automática:</strong> Al modificar oportunidades, se revisa toda la estructura</li>
+                            <li><strong>Seguridad:</strong> Las carpetas nunca se eliminan automáticamente</li>
+                            <li>Los documentos se organizarán automáticamente por empresa/contacto y oportunidad</li>
+                        </ul>
+                    </div>
+                </page>
+            </xpath>
+        </field>
+    </record>
+</odoo>