Browse Source

Merge subtree m22tc_google_workspace with latest changes - Add CRM meets files synchronization functionality - Resolve merge conflicts using latest version

root 5 months ago
parent
commit
d3b3612530

+ 69 - 0
m22tc_google_workspace/README.md

@@ -8,6 +8,7 @@ Módulo de integración con Google Workspace para CRM que permite gestionar docu
 - **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
+- **Sincronización automática con Google Meet**: Procesamiento automático de grabaciones de reuniones
 
 ## Dependencias
 
@@ -70,6 +71,74 @@ Google Drive CRM Folder (Principal)
 └── ...
 ```
 
+## Sincronización Automática con Google Meet
+
+### Funcionalidad de Sincronización Flexible
+
+El módulo incluye una función de sincronización que puede procesar reuniones de Google Meet de diferentes maneras:
+
+#### 1. Sincronización por Días (Entero)
+```python
+# Sincronizar meets de los últimos 15 días
+env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter=15)
+
+# Sincronizar meets de los últimos 7 días
+env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter=7)
+
+# Sincronizar meets del último día
+env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter=1)
+```
+
+#### 2. Sincronización por Fecha Específica (YYYY-MM-DD)
+```python
+# Sincronizar meets de una fecha específica
+env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter='2024-08-30')
+
+# Sincronizar meets de otra fecha
+env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter='2024-01-15')
+```
+
+#### 3. Sincronización con String como Entero
+```python
+# Sincronizar meets usando string como días
+env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter='3')
+env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter='10')
+```
+
+### Casos de Uso
+
+- **Para pruebas en producción**: Usar fecha específica (`'2024-08-30'`)
+- **Para sincronización diaria**: Usar entero (`1`)
+- **Para sincronización semanal**: Usar entero (`7`)
+- **Para sincronización mensual**: Usar entero (`30`)
+
+### Ejecución desde Línea de Comandos
+
+```bash
+# Ejemplo: Sincronizar meets de una fecha específica
+python3 -c "
+import odoo
+odoo.cli.server.main()
+env = odoo.api.Environment(odoo.registry('stg2.mcteam.run').cursor(), odoo.SUPERUSER_ID, {})
+result = env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter='2024-08-30')
+print(result)
+env.cr.close()
+"
+```
+
+### Proceso de Sincronización
+
+1. **Búsqueda de Reuniones**: Busca reuniones de Google Meet con grabaciones
+2. **Identificación de Participantes**: Extrae emails de participantes de las reuniones
+3. **Búsqueda de Oportunidades**: Encuentra oportunidades CRM donde los participantes son contactos
+4. **Creación de Estructura**: Crea carpetas de Google Drive si no existen
+5. **Movimiento de Archivos**: Mueve las grabaciones a la carpeta "Meets" de la oportunidad correspondiente
+
+### Configuración del Cron
+
+El cron está configurado para ejecutarse cada 30 minutos por defecto, pero puedes modificarlo en:
+**Ajustes > Técnico > Automatización > Tareas Programadas > "CRM Calendar Sync - Google Workspace"**
+
 ## Próximas Funcionalidades
 
 - Sincronización automática de documentos

+ 1 - 0
m22tc_google_workspace/__manifest__.py

@@ -20,6 +20,7 @@
         'security/ir.model.access.csv',
         'views/res_company_views.xml',
         'views/crm_lead_views.xml',
+        'views/res_users_views.xml',
         'data/m22tc_google_workspace_data.xml',
     ],
     'assets': {

+ 11 - 0
m22tc_google_workspace/data/m22tc_google_workspace_data.xml

@@ -6,5 +6,16 @@
             <field name="key">m22tc_google_workspace.drive_crm_enabled</field>
             <field name="value">False</field>
         </record>
+        
+        <!-- CRM Calendar Sync Cron Job -->
+        <record id="ir_cron_crm_calendar_sync" model="ir.cron">
+            <field name="name">CRM Calendar Sync - Google Workspace</field>
+            <field name="model_id" ref="model_crm_lead"/>
+            <field name="state">code</field>
+            <field name="code">model._sync_meetings_with_opportunities_cron(days_back=15)</field>
+            <field name="interval_number">30</field>
+            <field name="interval_type">minutes</field>
+            <field name="active" eval="False"/>
+        </record>
     </data>
 </odoo>

+ 143 - 0
m22tc_google_workspace/example_usage.py

@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+"""
+Ejemplo de uso de la nueva funcionalidad de sincronización CRM con Google Meet
+
+Este script muestra cómo usar la función de sincronización con diferentes tipos de parámetros:
+1. Entero (días hacia atrás)
+2. Fecha específica (formato YYYY-MM-DD)
+
+Uso:
+    python3 example_usage.py
+"""
+
+import sys
+import os
+
+# Agregar el directorio de Odoo al path
+sys.path.append('/var/odoo/stg2.mcteam.run/src')
+
+# Configurar variables de entorno
+os.environ['ODOO_RC'] = '/var/odoo/stg2.mcteam.run/odoo.conf'
+
+def test_sync_with_days_back():
+    """Ejemplo: Sincronizar meets de los últimos 7 días"""
+    print("🔄 Ejemplo 1: Sincronizar meets de los últimos 7 días")
+    print("=" * 60)
+    
+    try:
+        import odoo
+        from odoo import api, SUPERUSER_ID
+        
+        # Inicializar Odoo
+        odoo.cli.server.main()
+        
+        # Crear entorno
+        env = api.Environment(odoo.registry('stg2.mcteam.run').cursor(), SUPERUSER_ID, {})
+        
+        # Ejecutar sincronización con días hacia atrás
+        result = env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter=7)
+        
+        print(f"✅ Resultado: {result}")
+        
+    except Exception as e:
+        print(f"❌ Error: {str(e)}")
+    finally:
+        if 'env' in locals():
+            env.cr.close()
+
+def test_sync_with_specific_date():
+    """Ejemplo: Sincronizar meets de una fecha específica"""
+    print("\n🔄 Ejemplo 2: Sincronizar meets de una fecha específica")
+    print("=" * 60)
+    
+    try:
+        import odoo
+        from odoo import api, SUPERUSER_ID
+        
+        # Inicializar Odoo
+        odoo.cli.server.main()
+        
+        # Crear entorno
+        env = api.Environment(odoo.registry('stg2.mcteam.run').cursor(), SUPERUSER_ID, {})
+        
+        # Ejecutar sincronización con fecha específica
+        specific_date = "2024-08-30"  # Cambiar por la fecha que quieras probar
+        result = env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter=specific_date)
+        
+        print(f"✅ Resultado para fecha {specific_date}: {result}")
+        
+    except Exception as e:
+        print(f"❌ Error: {str(e)}")
+    finally:
+        if 'env' in locals():
+            env.cr.close()
+
+def test_sync_with_string_days():
+    """Ejemplo: Sincronizar meets usando string como días"""
+    print("\n🔄 Ejemplo 3: Sincronizar meets usando string como días")
+    print("=" * 60)
+    
+    try:
+        import odoo
+        from odoo import api, SUPERUSER_ID
+        
+        # Inicializar Odoo
+        odoo.cli.server.main()
+        
+        # Crear entorno
+        env = api.Environment(odoo.registry('stg2.mcteam.run').cursor(), SUPERUSER_ID, {})
+        
+        # Ejecutar sincronización con string de días
+        result = env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter="3")
+        
+        print(f"✅ Resultado para últimos 3 días: {result}")
+        
+    except Exception as e:
+        print(f"❌ Error: {str(e)}")
+    finally:
+        if 'env' in locals():
+            env.cr.close()
+
+def show_usage_examples():
+    """Mostrar ejemplos de uso"""
+    print("📋 EJEMPLOS DE USO DE LA NUEVA FUNCIONALIDAD")
+    print("=" * 60)
+    print()
+    print("La función ahora acepta dos tipos de parámetros:")
+    print()
+    print("1️⃣ ENTERO (días hacia atrás):")
+    print("   - time_filter=15     # Últimos 15 días")
+    print("   - time_filter=7      # Últimos 7 días")
+    print("   - time_filter=1      # Último día")
+    print()
+    print("2️⃣ FECHA ESPECÍFICA (formato YYYY-MM-DD):")
+    print("   - time_filter='2024-08-30'  # Solo meets del 30 de agosto de 2024")
+    print("   - time_filter='2024-08-31'  # Solo meets del 31 de agosto de 2024")
+    print("   - time_filter='2024-01-15'  # Solo meets del 15 de enero de 2024")
+    print()
+    print("3️⃣ STRING COMO ENTERO:")
+    print("   - time_filter='3'    # Últimos 3 días")
+    print("   - time_filter='10'   # Últimos 10 días")
+    print()
+    print("🔧 CÓMO USAR EN PRODUCCIÓN:")
+    print()
+    print("Desde la consola de Odoo:")
+    print(">>> env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter=15)")
+    print(">>> env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter='2024-08-30')")
+    print()
+    print("Desde línea de comandos:")
+    print("$ python3 -c \"import odoo; odoo.cli.server.main(); env = odoo.api.Environment(odoo.registry('stg2.mcteam.run').cursor(), odoo.SUPERUSER_ID, {}); result = env['crm.lead']._sync_meetings_with_opportunities_cron(time_filter='2024-08-30'); print(result); env.cr.close()\"")
+    print()
+    print("🎯 CASOS DE USO:")
+    print("- Para pruebas en producción: Usar fecha específica")
+    print("- Para sincronización diaria: Usar entero (1)")
+    print("- Para sincronización semanal: Usar entero (7)")
+    print("- Para sincronización mensual: Usar entero (30)")
+
+if __name__ == "__main__":
+    show_usage_examples()
+    
+    # Descomentar las líneas siguientes para ejecutar los ejemplos
+    # test_sync_with_days_back()
+    # test_sync_with_specific_date()
+    # test_sync_with_string_days()

+ 1 - 0
m22tc_google_workspace/models/__init__.py

@@ -1,2 +1,3 @@
 from . import res_company
 from . import crm_lead
+from . import res_users

BIN
m22tc_google_workspace/models/__pycache__/crm_lead.cpython-312.pyc


BIN
m22tc_google_workspace/models/__pycache__/res_company.cpython-312.pyc


File diff suppressed because it is too large
+ 403 - 981
m22tc_google_workspace/models/crm_lead.py


+ 1704 - 0
m22tc_google_workspace/models/crm_lead_backup.py

@@ -0,0 +1,1704 @@
+# -*- 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_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 using the new service"""
+        if not self.google_drive_folder_id or not self.company_id:
+            return False
+        
+        try:
+            # Get company root folder ID
+            company_root_folder_id = self._get_company_root_folder_id()
+            if not company_root_folder_id:
+                return False
+            
+            # Use the new Google Drive service
+            drive_service = self.env['google.drive.service']
+            
+            # Check if folder belongs to the company root
+            belongs_to_company = drive_service.check_folder_belongs_to_parent(
+                self.google_drive_folder_id, 
+                company_root_folder_id
+            )
+            
+            # Return True if there's a mismatch (folder doesn't belong to company)
+            return not belongs_to_company
+            
+        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 using the new service"""
+        try:
+            drive_service = self.env['google.drive.service']
+            validation = drive_service.validate_folder_id(folder_id)
+            
+            if validation.get('valid'):
+                folder_name = validation.get('name', 'Unknown')
+                _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
+                return True, folder_name
+            else:
+                error_message = validation.get('error', 'Unknown error')
+                _logger.warning(f"❌ Folder ID {folder_id} validation failed: {error_message}")
+                return False, error_message
+                
+        except Exception as e:
+            _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
+            return False, str(e)
+
+    def _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
+        
+        components = self._get_folder_name_components()
+        
+        try:
+            # Create folder structure in batch for better performance
+            folder_structure = self._create_folder_structure_batch(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, components):
+        """Create folder structure in batch for better performance using the new service"""
+        try:
+            drive_service = self.env['google.drive.service']
+            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_crm(
+                root_folder_id, components['primary_name']
+            )
+            
+            # Step 2: Create or get year folder
+            year_folder_id = self._create_or_get_folder_crm(
+                primary_folder_id, components['year']
+            )
+            
+            # Step 3: Create or get opportunity folder
+            opportunity_folder_id = self._create_or_get_folder_crm(
+                year_folder_id, components['opportunity_name']
+            )
+            
+            # Step 4: Create Meets and Archivos cliente folders (parallel creation)
+            meets_folder_id = self._create_or_get_folder_crm(
+                opportunity_folder_id, 'Meets'
+            )
+            
+            archivos_folder_id = self._create_or_get_folder_crm(
+                opportunity_folder_id, 'Archivos cliente'
+            )
+            
+            return {
+                'opportunity_folder_id': opportunity_folder_id,
+                'meets_folder_id': meets_folder_id,
+                'archivos_folder_id': archivos_folder_id
+            }
+        except Exception as e:
+            _logger.error(f"Error creating folder structure batch: {str(e)}")
+            raise
+
+    def _create_or_get_folder_crm(self, parent_folder_id, folder_name):
+        """Create a folder or get existing one by name using the new service"""
+        try:
+            drive_service = self.env['google.drive.service']
+            
+            # First, check if folder already exists
+            existing_folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
+            if existing_folders:
+                return existing_folders[0]['id']
+            
+            # Create new folder
+            result = drive_service.create_folder(folder_name, parent_folder_id)
+            
+            if result.get('success'):
+                return result.get('folder_id')
+            else:
+                error_msg = result.get('error', 'Unknown error')
+                raise UserError(_('Failed to create Google Drive folder "%s": %s') % (folder_name, error_msg))
+                
+        except Exception as e:
+            _logger.error(f"Error creating or getting folder {folder_name}: {str(e)}")
+            raise
+
+    def _find_folder_by_name(self, headers, parent_folder_id, folder_name):
+        """Find a folder by name in the parent folder with caching using the new service"""
+        try:
+            drive_service = self.env['google.drive.service']
+            
+            # Use the new service method
+            folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
+            
+            if folders:
+                result = folders[0]
+                
+                # Cache the result for 2 minutes
+                cache_key = f'folder_{parent_folder_id}_{folder_name}'
+                _GOOGLE_DRIVE_CACHE[cache_key] = {
+                    'result': result,
+                    'expires': datetime.now() + timedelta(seconds=120)
+                }
+                
+                return result
+            else:
+                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 using the new service"""
+        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
+        
+        try:
+            drive_service = self.env['google.drive.service']
+            
+            _logger.info(f"Renaming Google Drive folder {self.google_drive_folder_id} to '{sanitized_name}'")
+            
+            result = drive_service.rename_folder(self.google_drive_folder_id, sanitized_name)
+            
+            if result.get('success'):
+                # 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: {result.get("error", "Unknown error")}'
+                _logger.error(error_msg)
+                raise UserError(_(error_msg))
+                
+        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'
+        }
+        
+        try:
+            drive_service = self.env['google.drive.service']
+            
+            # Move the folder to the new company root
+            result = drive_service.move_folder(self.google_drive_folder_id, new_root_folder_id)
+            
+            if not result.get('success'):
+                raise UserError(_('Failed to move folder to new company: %s') % result.get('error', 'Unknown error'))
+                
+        except Exception as e:
+            _logger.error(f"Error moving folder to new company: {str(e)}")
+            raise UserError(_('Failed to move folder to new company: %s') % str(e))
+
+    def _delete_google_drive_folder_structure(self):
+        """Delete the Google Drive folder structure when contact is removed using the new service"""
+        if not self.google_drive_folder_id:
+            return
+        
+        try:
+            drive_service = self.env['google.drive.service']
+        
+        # Delete the folder (this will also delete subfolders)
+            result = drive_service.delete_folder(self.google_drive_folder_id)
+            
+            if result.get('success'):
+            # 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: %s') % result.get('error', 'Unknown error'))
+                
+        except Exception as e:
+            _logger.error(f"Error deleting folder structure: {str(e)}")
+            raise UserError(_('Failed to delete Google Drive folder structure: %s') % str(e))
+
+    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=None, new_components=None):
+        """Unified method to rename folder structure"""
+        if not self.google_drive_folder_id:
+            return
+        
+        try:
+            # If only new_components provided, get current from Google Drive
+            if old_components is None and new_components is not None:
+                current_structure = self._analyze_complete_folder_structure()
+                if not current_structure:
+                    raise UserError(_('Could not analyze current folder structure'))
+                
+                old_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', '')
+                }
+            
+            # If only old_components provided, get new from current record
+            if new_components is None and old_components is not None:
+                new_components = self._get_folder_name_components()
+            
+            drive_service = self.env['google.drive.service']
+        current_folder_id = self.google_drive_folder_id
+        
+            # Navigate up to find primary folder
+            primary_folder_id = self._find_primary_folder_id_crm(current_folder_id)
+        if not primary_folder_id:
+            raise UserError(_('Cannot find primary folder in the structure'))
+        
+            # Rename primary folder if needed
+        if old_components['primary_name'] != new_components['primary_name']:
+                result = drive_service.rename_folder(primary_folder_id, new_components['primary_name'])
+                if not result.get('success'):
+                    raise UserError(_('Failed to rename primary folder: %s') % result.get('error', 'Unknown error'))
+        
+            # Rename year folder if needed
+        if old_components['year'] != new_components['year']:
+                year_folder_id = self._find_year_folder_id_crm(primary_folder_id, old_components['year'])
+            if year_folder_id:
+                    result = drive_service.rename_folder(year_folder_id, new_components['year'])
+                    if not result.get('success'):
+                        raise UserError(_('Failed to rename year folder: %s') % result.get('error', 'Unknown error'))
+        
+        # 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']}'")
+                result = drive_service.rename_folder(current_folder_id, new_components['opportunity_name'])
+                if not result.get('success'):
+                    raise UserError(_('Failed to rename opportunity folder: %s') % result.get('error', 'Unknown error'))
+                
+                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']}'")
+
+        except Exception as e:
+            _logger.error(f"Error renaming folder structure: {str(e)}")
+            raise
+
+
+
+    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(vals, current_values[record.id])
+        
+        return result
+
+    def _process_google_drive_updates(self, vals, old_values=None):
+        """Unified method to process Google Drive updates"""
+        try:
+            _logger.info(f"Processing Google Drive updates for opportunity {self.id}")
+            
+            # Check if we need to create folder
+            should_create = self._should_create_folder(vals)
+            if should_create and self._validate_folder_creation_prerequisites():
+                _logger.info(f"Creating Google Drive folder for opportunity {self.id}")
+                        self._create_google_drive_folder_structure()
+                        if self._store_initial_structure_and_update_url():
+                            self.message_post(
+                                body=_("✅ Google Drive folder created automatically"),
+                                message_type='comment'
+                            )
+            
+            # If we have a folder, verify and update structure
+            if self.google_drive_folder_id:
+                self._verify_and_update_folder_structure(vals, old_values)
+                
+        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 _should_create_folder(self, vals):
+        """Helper method to determine if folder should be created"""
+        # Stage-based creation logic
+        if 'stage_id' in vals and not self.google_drive_folder_id:
+            return True
+        
+        # Auto-creation logic
+        if not self.google_drive_folder_id:
+            company = self.company_id
+            return (company.google_drive_crm_enabled and 
+                    company.google_drive_crm_stage_id and
+                    self.stage_id.id == company.google_drive_crm_stage_id.id)
+        
+        return False
+
+    def _verify_and_update_folder_structure(self, vals, old_values=None):
+        """Unified method to verify and update folder structure"""
+        try:
+            _logger.info(f"Processing Google Drive updates for opportunity {self.id}")
+            
+            # Get expected structure components
+            expected_components = self._get_folder_name_components()
+            expected_structure = self._build_structure_string(expected_components)
+            
+            # Get current structure from old_values or stored field
+            if old_values:
+                current_structure = old_values.get('google_drive_folder_name', '')
+            else:
+            current_structure = self.google_drive_folder_name or ''
+            
+            _logger.info(f"Structure comparison: Current='{current_structure}' vs 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 and self.google_drive_folder_id:
+                    new_company_id = vals['company_id']
+                    if old_values and 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)
+                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"),
+                    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:
+            # Use the unified method with only new_components
+            self._rename_entire_folder_structure(new_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 using the new service"""
+        try:
+            # Use the new Google Drive service
+            drive_service = self.env['google.drive.service']
+            
+            # Get current folder info and navigate up to find the primary folder (company/contact)
+            current_folder_id = self.google_drive_folder_id
+            
+            # Validate current folder using the service
+            validation = drive_service.validate_folder_id(current_folder_id)
+            if not validation.get('valid'):
+                raise UserError(_('Failed to get current folder information'))
+            
+            # Navigate up the hierarchy to find the primary folder
+            primary_folder_id = self._find_primary_folder_id_crm(current_folder_id)
+            
+            if not primary_folder_id:
+                raise UserError(_('Could not find primary folder in hierarchy'))
+            
+            # Move the primary folder to the new parent
+            result = drive_service.move_folder(primary_folder_id, new_parent_id)
+            
+            if not result.get('success'):
+                raise UserError(_('Failed to move folder structure to new parent: %s') % result.get('error', 'Unknown error'))
+            
+            _logger.info(f"Successfully moved entire folder structure 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 using the new service"""
+        return self._find_primary_folder_id_crm(start_folder_id)
+
+    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'
+            }
+            
+            # Use the new Google Drive service to get folder information
+            drive_service = self.env['google.drive.service']
+            validation = drive_service.validate_folder_id(self.google_drive_folder_id)
+            
+            if not validation.get('valid'):
+                raise UserError(_('Failed to get current folder information from Google Drive'))
+            
+            current_folder_name = validation.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 using the new service"""
+        current_folder_id = self.google_drive_folder_id
+        
+        try:
+            # Use the new CRM-specific method
+            return self._analyze_crm_folder_structure(current_folder_id)
+        except Exception as e:
+            _logger.error(f"Error analyzing current folder structure: {str(e)}")
+            return None
+
+    def _analyze_complete_folder_structure(self, headers):
+        """Analyze the complete folder structure from root to opportunity using the new CRM-specific method"""
+        current_folder_id = self.google_drive_folder_id
+        
+        try:
+            # Use the new CRM-specific method
+            return self._analyze_crm_folder_structure(current_folder_id)
+        except Exception as e:
+            _logger.error(f"Error in _analyze_complete_folder_structure: {str(e)}")
+            return None
+
+    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 using the new service"""
+        try:
+            drive_service = self.env['google.drive.service']
+            return drive_service.find_folders_by_name(parent_id, r'.*')
+        except Exception as e:
+            _logger.error(f"Error getting subfolders: {str(e)}")
+            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 using the new service"""
+        try:
+            drive_service = self.env['google.drive.service']
+            validation = drive_service.validate_folder_id(folder_id)
+            
+            if validation.get('valid'):
+                folder_name = validation.get('name', 'Unknown')
+                _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
+                return True, folder_name
+            else:
+                error_message = validation.get('error', 'Unknown error')
+                _logger.warning(f"❌ Folder ID {folder_id} validation failed: {error_message}")
+                return False, error_message
+                
+        except Exception as e:
+            _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
+            return False, str(e)
+
+    # ============================================================================
+    # MÉTODOS ESPECÍFICOS DE CRM QUE USAN SERVICIOS GENÉRICOS
+    # ============================================================================
+
+    def _find_primary_folder_id_crm(self, start_folder_id):
+        """Find the primary folder (company/contact level) in the hierarchy - CRM specific logic"""
+        try:
+            # Use the generic service to navigate the hierarchy
+            drive_service = self.env['google.drive.service']
+            hierarchy = drive_service.navigate_folder_hierarchy(start_folder_id, max_levels=5)
+            
+            # Apply CRM-specific logic to identify the primary folder
+            for folder_info in hierarchy:
+                folder_name = folder_info.get('name', '')
+                level = folder_info.get('level', 0)
+                
+                # Skip root level folders
+                if level == 0:
+                    continue
+                
+                # Check if this folder is the primary folder (not a year or opportunity folder)
+                # Primary folder is typically the company/contact 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 = drive_service.find_folders_by_name(folder_info['id'], r'^\d{4}$')
+                    if year_folders:
+                        # This is the primary folder
+                        return folder_info['id']
+            
+            return None
+            
+        except Exception as e:
+            _logger.error(f"Error finding primary folder (CRM): {str(e)}")
+            return None
+
+    def _find_year_folder_id_crm(self, primary_folder_id, year):
+        """Find the year folder within the primary folder - CRM specific"""
+        try:
+            drive_service = self.env['google.drive.service']
+            year_folders = drive_service.find_folders_by_name(primary_folder_id, f'^{year}$')
+            return year_folders[0]['id'] if year_folders else None
+        except Exception as e:
+            _logger.error(f"Error finding year folder (CRM): {str(e)}")
+            return None
+
+    def _analyze_crm_folder_structure(self, folder_id):
+        """Analyze the complete folder structure from root to opportunity - CRM specific"""
+        try:
+            drive_service = self.env['google.drive.service']
+            
+            # Get current folder info
+            validation = drive_service.validate_folder_id(folder_id)
+            if not validation.get('valid'):
+                return None
+            
+            current_name = validation.get('name', '')
+            
+            # Build complete structure
+            complete_structure = {
+                'opportunity_folder': {
+                    'id': folder_id,
+                    'name': current_name,
+                    'level': 3
+                }
+            }
+            
+            # Navigate up the hierarchy using the generic service
+            hierarchy = drive_service.navigate_folder_hierarchy(folder_id, max_levels=5)
+            
+            # Apply CRM-specific logic to categorize folders
+            for folder_info in hierarchy:
+                folder_name = folder_info.get('name', '')
+                level = folder_info.get('level', 0)
+                
+                if level == 2:  # Year level
+                    if folder_name.isdigit() and len(folder_name) == 4:
+                        complete_structure['year_folder'] = {
+                            'id': folder_info['id'],
+                            'name': folder_name,
+                            'level': level
+                        }
+                elif level == 1:  # Primary level (company/contact)
+                    if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
+                        complete_structure['primary_folder'] = {
+                            'id': folder_info['id'],
+                            'name': folder_name,
+                            'level': level
+                        }
+                elif level == 0:  # Root level
+                    complete_structure['root_folder'] = {
+                        'id': folder_info['id'],
+                        'name': folder_name,
+                        'level': level
+                    }
+            
+            return complete_structure
+            
+        except Exception as e:
+            _logger.error(f"Error in _analyze_crm_folder_structure: {str(e)}")
+            return None
+
+    def _check_crm_folder_company_mismatch(self, folder_id, company_root_id):
+        """Check if folder belongs to the correct company root - CRM specific"""
+        try:
+            drive_service = self.env['google.drive.service']
+            
+            # Use the generic service to check folder parent relationship
+            belongs_to_company = drive_service.check_folder_belongs_to_parent(
+                folder_id, 
+                company_root_id
+            )
+            
+            # Return True if there's a mismatch (folder doesn't belong to company)
+            return not belongs_to_company
+            
+        except Exception as e:
+            _logger.error(f"Error checking CRM folder company mismatch: {str(e)}")
+            return False
+
+

+ 89 - 219
m22tc_google_workspace/models/res_company.py

@@ -43,7 +43,7 @@ class ResCompany(models.Model):
             self.google_drive_crm_folder_name = False
 
     def action_test_google_drive_connection(self):
-        """Test Google Drive connection for this company"""
+        """Test Google Drive connection for this company using the new service"""
         self.ensure_one()
         
         if not self.google_drive_crm_enabled:
@@ -53,276 +53,146 @@ class ResCompany(models.Model):
             raise UserError(_('Please set a Google Drive CRM Folder ID first'))
         
         try:
-            import requests
-            import json
+            # Use the new Google Drive service
+            drive_service = self.env['google.drive.service']
             
-            # 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', '')
+            # Validate the folder ID using the new service
+            validation = drive_service.validate_folder_id(self.google_drive_crm_folder_id)
             
-            if not google_api_enabled or google_api_enabled == 'False':
-                raise UserError(_('Google API Integration is not enabled in system settings'))
+            if not validation.get('valid'):
+                raise UserError(_('Google Drive connection test failed: %s') % validation.get('error'))
             
-            if not google_api_client_id or not google_api_client_secret:
-                raise UserError(_('Google API credentials are not configured in system settings'))
+            folder_name = validation.get('name', 'Unknown')
+            folder_type = validation.get('type', 'Unknown')
             
-            # 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.'))
+            # Update the folder name in the record
+            self.google_drive_crm_folder_name = folder_name
             
-            # 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 - try both regular Drive and Shared Drive endpoints
-            folder_found = False
-            folder_name = 'Unknown'
-            
-            # First, try the regular Drive API endpoint
-            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')
-                folder_found = True
-            elif drive_response.status_code == 404:
-                # If not found in regular Drive, try Shared Drive endpoint
-                shared_drive_api_url = f"https://www.googleapis.com/drive/v3/drives/{self.google_drive_crm_folder_id}"
-                shared_drive_response = requests.get(shared_drive_api_url, headers=headers, timeout=10)
-                
-                if shared_drive_response.status_code == 200:
-                    shared_drive_data = shared_drive_response.json()
-                    folder_name = shared_drive_data.get('name', 'Unknown')
-                    folder_found = True
-                    
-                    # For shared drives, we also need to check if we can access files within it
-                    files_in_drive_url = f"https://www.googleapis.com/drive/v3/files?supportsAllDrives=true&includeItemsFromAllDrives=true&corpora=drive&driveId={self.google_drive_crm_folder_id}&pageSize=1"
-                    files_response = requests.get(files_in_drive_url, headers=headers, timeout=10)
-                    
-                    if files_response.status_code != 200:
-                        raise UserError(_('Access denied to Shared Drive. Please check your permissions in the Shared Drive.'))
-                elif shared_drive_response.status_code == 403:
-                    raise UserError(_('Access denied to Shared Drive. Please check your permissions in the Shared Drive.'))
-                elif shared_drive_response.status_code == 404:
-                    raise UserError(_('Google Drive folder not found. Please verify the Folder ID is correct. This could be a regular folder or Shared Drive.'))
-                else:
-                    raise UserError(_('Shared Drive API test failed. Status: %s') % shared_drive_response.status_code)
-            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)
-            
-            if folder_found:
-                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,
-                    }
+            return {
+                'type': 'ir.actions.client',
+                'tag': 'display_notification',
+                'params': {
+                    'title': _('Success'),
+                    'message': _('Google Drive connection test successful! Folder "%s" (%s) is accessible.') % (folder_name, folder_type),
+                    'type': 'success',
+                    'sticky': False,
                 }
+            }
             
-        except requests.exceptions.Timeout:
-            raise UserError(_('Connection timeout. Please check your internet connection.'))
-        except requests.exceptions.ConnectionError:
-            raise UserError(_('Connection error. Please check your internet connection.'))
         except Exception as e:
             raise UserError(_('Google Drive connection test failed: %s') % str(e))
 
     def action_open_google_drive_folder(self):
-        """Open Google Drive folder in browser"""
+        """Open Google Drive folder in browser using the new service"""
         self.ensure_one()
         
         if not self.google_drive_crm_folder_id:
             raise UserError(_('No Google Drive CRM folder configured'))
         
-        # For shared drives, the URL format is different
-        # Try to determine if it's a shared drive by checking the API
         try:
-            import requests
+            # Use the new Google Drive service
+            drive_service = self.env['google.drive.service']
             
-            access_token = self.env['ir.config_parameter'].sudo().get_param('google_api.access_token')
-            if access_token:
-                headers = {'Authorization': f'Bearer {access_token}'}
-                
-                # First try regular 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=5)
-                
-                if drive_response.status_code == 404:
-                    # Try shared drive
-                    shared_drive_api_url = f"https://www.googleapis.com/drive/v3/drives/{self.google_drive_crm_folder_id}"
-                    shared_drive_response = requests.get(shared_drive_api_url, headers=headers, timeout=5)
-                    
-                    if shared_drive_response.status_code == 200:
-                        # It's a shared drive
-                        folder_url = f"https://drive.google.com/drive/u/0/folders/{self.google_drive_crm_folder_id}"
-                    else:
-                        # Regular folder
-                        folder_url = f"https://drive.google.com/drive/folders/{self.google_drive_crm_folder_id}"
-                else:
-                    # Regular folder
-                    folder_url = f"https://drive.google.com/drive/folders/{self.google_drive_crm_folder_id}"
-            else:
-                # Fallback to regular folder URL
-                folder_url = f"https://drive.google.com/drive/folders/{self.google_drive_crm_folder_id}"
+            # Get the folder URL using the new service
+            folder_url = drive_service.get_folder_url(self.google_drive_crm_folder_id)
+            
+            if not folder_url:
+                raise UserError(_('Could not generate folder URL'))
+            
+            return {
+                'type': 'ir.actions.act_url',
+                'url': folder_url,
+                'target': 'new',
+            }
                 
-        except Exception:
-            # Fallback to regular folder URL
-            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',
-        }
+        except Exception as e:
+            raise UserError(_('Failed to open Google Drive folder: %s') % str(e))
 
     def action_create_google_drive_folder(self):
-        """Create a folder in Google Drive using manual OAuth flow"""
+        """Create a test folder in Google Drive using the new service"""
         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.'))
+        if not self.google_drive_crm_folder_id:
+            raise UserError(_('Please set a Google Drive CRM Folder ID 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'
-            }
+            # Use the new Google Drive service
+            drive_service = self.env['google.drive.service']
             
-            # Create folder metadata
+            # Create folder using the new service
             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 - check if parent is a shared drive and adjust accordingly
-            is_shared_drive = False
-            
-            # Check if the parent folder is a shared drive
-            if self.google_drive_crm_folder_id:
-                # Try to get parent folder info to determine if it's a shared drive
-                parent_check_url = f"https://www.googleapis.com/drive/v3/files/{self.google_drive_crm_folder_id}"
-                parent_response = requests.get(parent_check_url, headers=headers, timeout=10)
-                
-                if parent_response.status_code == 404:
-                    # Parent might be a shared drive, try shared drive endpoint
-                    shared_drive_check_url = f"https://www.googleapis.com/drive/v3/drives/{self.google_drive_crm_folder_id}"
-                    shared_drive_response = requests.get(shared_drive_check_url, headers=headers, timeout=10)
-                    
-                    if shared_drive_response.status_code == 200:
-                        is_shared_drive = True
-                        # For shared drives, we need to add specific parameters
-                        folder_metadata['supportsAllDrives'] = True
-                        folder_metadata['includeItemsFromAllDrives'] = True
             
-            # Create folder
-            response = requests.post(
-                'https://www.googleapis.com/drive/v3/files',
-                headers=headers,
-                json=folder_metadata,
-                timeout=30
+            result = drive_service.create_folder(
+                name=folder_name,
+                parent_folder_id=self.google_drive_crm_folder_id,
+                description="Test folder created by Odoo CRM integration using new service"
             )
             
-            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,
-                    }
+            if not result.get('success'):
+                raise UserError(_('Failed to create folder: %s') % result.get('error'))
+            
+            return {
+                'type': 'ir.actions.client',
+                'tag': 'display_notification',
+                'params': {
+                    'title': _('Success'),
+                    'message': _('Google Drive folder created successfully: %s') % result.get('folder_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"""
+        """List folders in Google Drive using the new service"""
         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.'))
+        if not self.google_drive_crm_folder_id:
+            raise UserError(_('Please set a Google Drive CRM Folder ID first'))
         
         try:
-            import requests
+            # Use the new Google Drive service
+            drive_service = self.env['google.drive.service']
             
-            headers = {
-                'Authorization': f'Bearer {access_token}',
-            }
+            # List folders using the new service
+            result = drive_service.list_folders(
+                parent_folder_id=self.google_drive_crm_folder_id,
+                include_shared_drives=True
+            )
             
-            # List folders - include both regular folders and shared drives
-            params = {
-                'q': "mimeType='application/vnd.google-apps.folder' and trashed=false",
-                'fields': 'files(id,name,webViewLink)',
-                'pageSize': 10,
-                'supportsAllDrives': 'true',
-                'includeItemsFromAllDrives': 'true'
-            }
+            if not result.get('success'):
+                raise UserError(_('Failed to list folders: %s') % result.get('error'))
             
-            response = requests.get(
-                'https://www.googleapis.com/drive/v3/files',
-                headers=headers,
-                params=params,
-                timeout=30
-            )
+            folders = result.get('folders', [])
             
-            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.'))
+            if not folders:
+                message = _('No folders found in the specified Google Drive location.')
             else:
-                raise UserError(_('Failed to list Google Drive folders. Status: %s') % response.status_code)
-                
+                folder_names = [folder.get('name', 'Unknown') for folder in folders]
+                message = _('Found %d folders: %s') % (len(folders), ', '.join(folder_names))
+            
+            return {
+                'type': 'ir.actions.client',
+                'tag': 'display_notification',
+                'params': {
+                    'title': _('Google Drive Folders'),
+                    'message': message,
+                    'type': 'info',
+                    'sticky': False,
+                }
+            }
+            
         except Exception as e:
             raise UserError(_('Failed to list Google Drive folders: %s') % str(e))
+
+    # CRM Calendar Sync methods removed

+ 14 - 0
m22tc_google_workspace/models/res_users.py

@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class ResUsers(models.Model):
+    _inherit = "res.users"
+
+    # CRM sync method removed

+ 1 - 5
m22tc_google_workspace/views/crm_lead_views.xml

@@ -38,11 +38,7 @@
                                 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>

+ 1 - 0
m22tc_google_workspace/views/res_company_views.xml

@@ -41,6 +41,7 @@
                                 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">

+ 16 - 0
m22tc_google_workspace/views/res_users_views.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Extend user preferences view to add CRM sync button -->
+        <record id="view_users_preferences_form_crm_sync" model="ir.ui.view">
+            <field name="name">res.users.preferences.form.crm.sync</field>
+            <field name="model">res.users</field>
+            <field name="inherit_id" ref="google_api.view_users_preferences_form_google_connect"/>
+            <field name="arch" type="xml">
+                <xpath expr="//group[@name='google_connect_group']" position="inside">
+                    <!-- CRM Sync button removed -->
+                </xpath>
+            </field>
+        </record>
+    </data>
+</odoo>

Some files were not shown because too many files changed in this diff