# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging import re from datetime import datetime, timedelta from odoo import fields, models, api, _ from odoo.exceptions import UserError _logger = logging.getLogger(__name__) # Cache for Google Drive API responses (5 minutes) _GOOGLE_DRIVE_CACHE = {} 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: 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 count = 0 _GOOGLE_DRIVE_CACHE[cache_key] = { 'count': count, 'expires': datetime.now() + timedelta(seconds=300) } record.google_drive_documents_count = count else: record.google_drive_documents_count = 0 # ============================================================================ # MÉTODOS DE CONFIGURACIÓN Y UTILIDADES # ============================================================================ def _get_company_root_folder_id(self): """Get the company's root Google Drive folder ID""" company = self.company_id return (company.google_drive_crm_folder_id if company and company.google_drive_crm_enabled else None) def _extract_folder_id_from_url(self, url): """Extract folder ID from Google Drive URL - Now uses generic service""" drive_service = self.env['google.drive.service'] return drive_service.extract_folder_id_from_url(url) def _get_configured_field_value(self): """Get value from the configured Google Drive field in company settings""" field_id = self.company_id.google_drive_crm_field_id return getattr(self, field_id.name, None) if field_id else None def _set_configured_field_value(self, value): """Set the value of the configured field""" field_id = self.company_id.google_drive_crm_field_id if field_id: self.with_context(skip_google_drive_update=True).write({field_id.name: value}) return True return False def _sanitize_folder_name(self, name): """Sanitize folder name to be Google Drive compatible - Now uses generic service""" drive_service = self.env['google.drive.service'] return drive_service.sanitize_folder_name(name) 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 _update_folder_structure_fields(self, folder_id=None, structure_components=None): """Helper method to update folder structure fields without triggering loops""" update_vals = {} if folder_id: update_vals['google_drive_folder_id'] = folder_id update_vals['google_drive_url'] = f"https://drive.google.com/drive/folders/{folder_id}" if structure_components: update_vals['google_drive_folder_name'] = self._build_structure_string(structure_components) if update_vals: self.with_context(skip_google_drive_update=True).write(update_vals) def _post_folder_message(self, message, message_type='comment'): """Helper method to post folder-related messages""" self.message_post( body=_(message), message_type=message_type ) def _get_current_folder_components(self): """Helper method to get current folder components""" return self._get_folder_name_components() def _get_expected_folder_structure(self): """Helper method to get expected folder structure string""" components = self._get_current_folder_components() return self._build_structure_string(components) # ============================================================================ # MÉTODOS DE VALIDACIÓN Y PRERREQUISITOS # ============================================================================ def _validate_folder_creation_prerequisites(self): """Validate prerequisites before creating Google Drive folder""" if not self._get_company_root_folder_id(): self.message_post( body=_("⚠️ Google Drive folder creation skipped: Company doesn't have Google Drive configured."), message_type='comment' ) return False if not self.partner_id: self.message_post( body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity."), message_type='comment' ) return False return True def _try_get_existing_folder_id(self): """Try to get existing folder ID from various sources""" # Check if we already have a folder ID if self.google_drive_folder_id: return self.google_drive_folder_id # Try to extract from configured field field_value = self._get_configured_field_value() if field_value: folder_id = self._extract_folder_id_from_url(field_value) if folder_id: _logger.info(f"Found folder ID from configured field: {folder_id}") return folder_id # Try to extract from google_drive_url field if self.google_drive_url: folder_id = self._extract_folder_id_from_url(self.google_drive_url) if folder_id: _logger.info(f"Found folder ID from URL field: {folder_id}") return folder_id return None def _validate_folder_id_with_google_drive(self, folder_id): """Validate if the folder ID exists and is accessible in Google Drive - Now uses generic service""" drive_service = self.env['google.drive.service'] return drive_service.validate_folder_id_with_google_drive(folder_id) # ============================================================================ # MÉTODOS DE COMPONENTES DE CARPETA # ============================================================================ def _get_folder_name_components(self): """Get the components for folder naming based on partner/contact information""" # Priority 1: partner_id.parent_id.name (empresa padre del contacto) if self.partner_id and self.partner_id.parent_id and self.partner_id.parent_id.name: primary_name = self.partner_id.parent_id.name # Priority 2: partner_id.company_name elif self.partner_id and self.partner_id.company_name: primary_name = self.partner_id.company_name # Priority 3: partner_id.name elif self.partner_id: primary_name = self.partner_id.name else: primary_name = "Sin Contacto" if not primary_name: raise UserError(_('No company or contact name available. Please assign a contact with company information.')) return { 'primary_name': self._sanitize_folder_name(primary_name), 'opportunity_name': self._sanitize_folder_name(self.name), 'year': str(self.create_date.year) if self.create_date else str(datetime.now().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 or old_partner.id == new_partner_id: return False new_partner = self.env['res.partner'].browse(new_partner_id) # 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 # ============================================================================ # MÉTODOS DE CREACIÓN DE CARPETAS # ============================================================================ def _create_google_drive_folder_structure(self): """Create the complete Google Drive folder structure for this opportunity""" self.ensure_one() 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: is_valid, _ = self._validate_folder_id_with_google_drive(existing_folder_id) if is_valid: self._store_folder_info(existing_folder_id) self.message_post( body=_("✅ Using existing Google Drive folder: %s") % existing_folder_id, message_type='comment' ) return True # Check if structure already exists before creating existing_structure = self._find_existing_folder_structure() if existing_structure: self._store_folder_info(existing_structure['opportunity_folder_id']) self.message_post( body=_("✅ Using existing folder structure: %s") % existing_structure['opportunity_folder_id'], message_type='comment' ) return existing_structure # Create new folder structure components = self._get_folder_name_components() try: folder_structure = self._create_folder_structure_batch(components) self._store_folder_info(folder_structure['opportunity_folder_id']) 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, reusing existing levels to avoid duplicates""" drive_service = self.env['google.drive.service'] root_folder_id = self._get_company_root_folder_id() # Level 1: Primary folder (empresa cliente) - reutilizar si existe primary_folder_id, was_reused = self._create_or_get_folder_crm(root_folder_id, components['primary_name']) _logger.info(f"✅ Primary folder '{components['primary_name']}': {'REUSED' if was_reused else 'CREATED'} (ID: {primary_folder_id})") # Level 2: Year folder - reutilizar si existe year_folder_id, was_reused = self._create_or_get_folder_crm(primary_folder_id, components['year']) _logger.info(f"✅ Year folder '{components['year']}': {'REUSED' if was_reused else 'CREATED'} (ID: {year_folder_id})") # Level 3: Opportunity folder - siempre crear (único por oportunidad) opportunity_folder_id, was_reused = self._create_or_get_folder_crm(year_folder_id, components['opportunity_name']) _logger.info(f"✅ Opportunity folder '{components['opportunity_name']}': {'REUSED' if was_reused else 'CREATED'} (ID: {opportunity_folder_id})") # Subfolders - reutilizar si existen 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 } def _create_or_get_folder_crm(self, parent_folder_id, folder_name): """Create a folder or get existing one by name, avoiding duplicates - Now uses generic service""" drive_service = self.env['google.drive.service'] _logger.info(f"🔍 Checking for existing folder '{folder_name}' in parent {parent_folder_id}") # First check if folder already exists existing_folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$') was_reused = len(existing_folders) > 0 if was_reused: folder_id = existing_folders[0]['id'] _logger.info(f"🔄 REUSING existing folder '{folder_name}' (ID: {folder_id})") else: # Create new folder result = drive_service.create_folder(folder_name, parent_folder_id) if result.get('success'): folder_id = result.get('folder_id') _logger.info(f"📁 CREATED new folder '{folder_name}' (ID: {folder_id})") else: error_msg = result.get('error', 'Unknown error') raise UserError(_('Failed to create Google Drive folder "%s": %s') % (folder_name, error_msg)) return folder_id, was_reused def _find_existing_folder_structure(self): """Find existing folder structure to avoid creating duplicates""" components = self._get_folder_name_components() drive_service = self.env['google.drive.service'] root_folder_id = self._get_company_root_folder_id() try: # Level 1: Find primary folder (empresa cliente) primary_folders = drive_service.find_folders_by_name(root_folder_id, f"^{components['primary_name']}$") if not primary_folders: return None primary_folder_id = primary_folders[0]['id'] # Level 2: Find year folder year_folders = drive_service.find_folders_by_name(primary_folder_id, f"^{components['year']}$") if not year_folders: return None year_folder_id = year_folders[0]['id'] # Level 3: Find opportunity folder opportunity_folders = drive_service.find_folders_by_name(year_folder_id, f"^{components['opportunity_name']}$") if not opportunity_folders: return None opportunity_folder_id = opportunity_folders[0]['id'] # Check if subfolders exist meets_folders = drive_service.find_folders_by_name(opportunity_folder_id, r'^Meets$') archivos_folders = drive_service.find_folders_by_name(opportunity_folder_id, r'^Archivos cliente$') _logger.info(f"🔄 Found existing complete folder structure for opportunity '{components['opportunity_name']}'") return { 'opportunity_folder_id': opportunity_folder_id, 'meets_folder_id': meets_folders[0]['id'] if meets_folders else None, 'archivos_folder_id': archivos_folders[0]['id'] if archivos_folders else None } except Exception as e: _logger.warning(f"Error finding existing folder structure: {str(e)}") return None def _store_folder_info(self, folder_id): """Store folder information and update URL""" expected_components = self._get_current_folder_components() # Update folder structure fields self._update_folder_structure_fields( folder_id=folder_id, structure_components=expected_components ) # Copy URL to configured field if empty if not self._get_configured_field_value(): self._set_configured_field_value(self.google_drive_url) # ============================================================================ # MÉTODOS DE ACTUALIZACIÓN Y RENOMBRADO # ============================================================================ def _process_google_drive_updates(self, vals, old_values=None): """Unified method to process Google Drive updates""" try: # Check if we need to create folder if self._should_create_folder(vals) and self._validate_folder_creation_prerequisites(): self._create_google_drive_folder_structure() self._post_folder_message("✅ Google Drive folder created automatically") # 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._post_folder_message(f"❌ Error updating Google Drive: {str(e)}") def _should_create_folder(self, vals): """Helper method to determine if folder should be created""" if 'stage_id' in vals and not self.google_drive_folder_id: return True 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""" # Handle company change (move folder to new company, don't rename) 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'): self._move_folder_to_new_company(new_company_id) # Don't update folder name - just move it return # Exit early after moving folder # Handle partner changes (this should trigger folder rename) if 'partner_id' in vals and self.google_drive_folder_id: old_partner_id = old_values.get('partner_id') if old_values else self.partner_id.id new_partner_id = vals['partner_id'] if old_partner_id != new_partner_id: # Partner changed - rename folder structure self._rename_entire_folder_structure() # Update stored structure expected_components = self._get_current_folder_components() self._update_folder_structure_fields(structure_components=expected_components) self._post_folder_message("✅ Google Drive folder renamed due to partner change") return # Handle partner parent changes (when contact's company changes) if 'partner_id' in vals and self.google_drive_folder_id: old_partner_id = old_values.get('partner_id') if old_values else self.partner_id.id new_partner_id = vals['partner_id'] if old_partner_id == new_partner_id: # Same partner, but check if their parent (company) changed old_partner = self.env['res.partner'].browse(old_partner_id) if old_partner_id else None new_partner = self.env['res.partner'].browse(new_partner_id) if new_partner_id else None if old_partner and new_partner: old_parent_id = old_partner.parent_id.id if old_partner.parent_id else None new_parent_id = new_partner.parent_id.id if new_partner.parent_id else None if old_parent_id != new_parent_id: # Partner's parent (company) changed - rename folder structure _logger.info(f"🔄 Partner's parent company changed from {old_parent_id} to {new_parent_id}. Renaming folder structure.") self._rename_entire_folder_structure() # Update stored structure expected_components = self._get_current_folder_components() self._update_folder_structure_fields(structure_components=expected_components) self._post_folder_message("✅ Google Drive folder renamed due to contact's company change") return # Handle other structure changes (name changes, etc.) expected_components = self._get_current_folder_components() expected_structure = self._get_expected_folder_structure() current_structure = (old_values.get('google_drive_folder_name', '') if old_values else self.google_drive_folder_name or '') if current_structure != expected_structure: # Rename structure self._rename_entire_folder_structure(new_components=expected_components) # Update stored structure self._update_folder_structure_fields(structure_components=expected_components) self._post_folder_message("✅ Google Drive folder structure updated") def _rename_entire_folder_structure(self, old_components=None, new_components=None): """Unified method to rename folder structure with robust error handling""" if not self.google_drive_folder_id: return try: drive_service = self.env['google.drive.service'] _logger.info(f"🔄 Starting folder structure rename for opportunity {self.id}") # First validate that the current folder exists validation = drive_service.validate_folder_id(self.google_drive_folder_id) if not validation.get('valid'): _logger.warning(f"Cannot rename folder {self.google_drive_folder_id}: folder not found or not accessible") raise UserError(_('Cannot rename folder: folder not found or not accessible')) # Get current structure if needed if old_components is None and new_components is not None: current_structure = self._analyze_crm_folder_structure(self.google_drive_folder_id) 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', '') } # Get new components if needed if new_components is None: new_components = self._get_current_folder_components() # Validate that we have valid components if not new_components: raise UserError(_('Could not determine new folder structure components')) current_folder_id = self.google_drive_folder_id # SOLUCIÓN ROBUSTA: En lugar de buscar el primary folder, vamos a recrear la estructura # Esto evita problemas con folders que ya no existen _logger.info(f"🔄 Using robust approach: recreating folder structure") # Get company root folder company_root_folder_id = self._get_company_root_folder_id() if not company_root_folder_id: raise UserError(_('Company root folder not configured')) # Create new structure with new components _logger.info(f"🔄 Creating new folder structure with components: {new_components}") # Create primary folder new_primary_folder_id, was_reused = self._create_or_get_folder_crm(company_root_folder_id, new_components['primary_name']) _logger.info(f"✅ Primary folder created/found: {new_primary_folder_id}") # Create year folder new_year_folder_id, was_reused = self._create_or_get_folder_crm(new_primary_folder_id, new_components['year']) _logger.info(f"✅ Year folder created/found: {new_year_folder_id}") # Validate current opportunity folder exists before moving _logger.info(f"🔍 Validating current opportunity folder: {current_folder_id}") current_validation = drive_service.validate_folder_id(current_folder_id) if not current_validation.get('valid'): _logger.warning(f"⚠️ Current opportunity folder {current_folder_id} is not accessible: {current_validation.get('error')}") # If folder doesn't exist, we need to create a new one _logger.info(f"🔄 Creating new opportunity folder in the correct structure") # Create new opportunity folder with correct name new_opportunity_folder_id, was_reused = self._create_or_get_folder_crm(new_year_folder_id, new_components['opportunity_name']) # Update the stored folder ID to the new folder self._update_folder_structure_fields( folder_id=new_opportunity_folder_id, structure_components=new_components ) _logger.info(f"✅ Created new opportunity folder: {new_opportunity_folder_id}") return # Move current opportunity folder to new structure _logger.info(f"🔄 Moving opportunity folder from old structure to new structure") move_result = drive_service.move_folder(current_folder_id, new_year_folder_id) if not move_result.get('success'): _logger.error(f"❌ Failed to move opportunity folder: {move_result.get('error')}") raise UserError(_('Failed to move opportunity folder: %s') % move_result.get('error', 'Unknown error')) _logger.info(f"✅ Successfully moved opportunity folder to new structure") # Update the stored folder information self._update_folder_structure_fields(structure_components=new_components) _logger.info(f"✅ Folder structure rename completed successfully") except Exception as e: _logger.error(f"Error renaming folder structure: {str(e)}") raise def _move_folder_to_new_company(self, new_company_id): """Move only this opportunity's folder to new company""" if not self.google_drive_folder_id: return 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._post_folder_message("⚠️ No se puede mover: Nueva empresa no tiene Google Drive configurado.") return try: drive_service = self.env['google.drive.service'] # First validate that the current folder exists validation = drive_service.validate_folder_id(self.google_drive_folder_id) if not validation.get('valid'): _logger.warning(f"Cannot move folder {self.google_drive_folder_id}: folder not found or not accessible") self._post_folder_message("⚠️ No se puede mover: La carpeta actual no existe o no es accesible.") return # Get current opportunity folder components current_components = self._get_current_folder_components() # Create new structure in the new company new_primary_folder_id, _ = self._create_or_get_folder_crm(new_root_folder_id, current_components['primary_name']) new_year_folder_id, _ = self._create_or_get_folder_crm(new_primary_folder_id, current_components['year']) # Move only the opportunity folder (not the entire primary folder) result = drive_service.move_folder(self.google_drive_folder_id, new_year_folder_id) if not result.get('success'): raise UserError(_('Failed to move folder to new company: %s') % result.get('error', 'Unknown error')) # Update the stored folder ID to the new location self._update_folder_structure_fields( folder_id=self.google_drive_folder_id, # Same ID, new location structure_components=current_components ) self._post_folder_message(f"✅ Oportunidad movida a nueva empresa: {new_company.name}") except Exception as e: _logger.error(f"Error moviendo folder: {str(e)}") # Don't raise the exception, just log it and continue self._post_folder_message(f"⚠️ Error moviendo carpeta: {str(e)}") # ============================================================================ # MÉTODOS ESPECÍFICOS DE CRM # ============================================================================ def _find_primary_folder_id_crm(self, start_folder_id): """Find the primary folder (company/contact level) in the hierarchy""" try: drive_service = self.env['google.drive.service'] _logger.info(f"🔍 Finding primary folder starting from: {start_folder_id}") # First validate the start folder exists validation = drive_service.validate_folder_id(start_folder_id) if not validation.get('valid'): _logger.error(f"❌ Start folder {start_folder_id} is not valid: {validation.get('error')}") return None hierarchy = drive_service.navigate_folder_hierarchy(start_folder_id, max_levels=5) _logger.info(f"📁 Hierarchy found: {len(hierarchy)} levels") for folder_info in hierarchy: folder_name = folder_info.get('name', '') level = folder_info.get('level', 0) folder_id = folder_info.get('id', '') _logger.info(f"📂 Level {level}: {folder_name} (ID: {folder_id})") if level == 0: continue if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']: # Validate this folder exists before checking its children folder_validation = drive_service.validate_folder_id(folder_id) if not folder_validation.get('valid'): _logger.warning(f"⚠️ Folder {folder_id} ({folder_name}) is not accessible, skipping") continue year_folders = drive_service.find_folders_by_name(folder_id, r'^\d{4}$') if year_folders: _logger.info(f"✅ Found primary folder: {folder_name} (ID: {folder_id})") return folder_id else: _logger.info(f"ℹ️ Folder {folder_name} has no year folders, not primary") _logger.warning(f"❌ No primary folder found in hierarchy") 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""" 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 - COMPLETELY REWRITTEN""" try: drive_service = self.env['google.drive.service'] # Step 1: Validate the current folder validation = drive_service.validate_folder_id(folder_id) if not validation.get('valid'): _logger.warning(f"Folder {folder_id} is not valid or accessible") return None current_name = validation.get('name', '') _logger.info(f"🔍 Starting analysis for folder: {current_name} (ID: {folder_id})") # Step 2: Get the complete hierarchy hierarchy = drive_service.navigate_folder_hierarchy(folder_id, max_levels=10) _logger.info(f"📊 Retrieved hierarchy: {len(hierarchy)} levels") if not hierarchy: _logger.warning(f"Could not retrieve hierarchy for folder {folder_id}") return { 'opportunity_folder': { 'id': folder_id, 'name': current_name, 'level': 0, 'found': True }, 'year_folder': {'found': False}, 'primary_folder': {'found': False}, 'root_folder': {'found': False} } # Step 3: Initialize structure with all components as not found structure = { 'opportunity_folder': {'found': False}, 'year_folder': {'found': False}, 'primary_folder': {'found': False}, 'root_folder': {'found': False} } # Step 4: Analyze each level in the hierarchy for folder_info in hierarchy: folder_name = folder_info.get('name', '') level = folder_info.get('level', 0) folder_id_info = folder_info.get('id', '') _logger.info(f"📂 Analyzing Level {level}: '{folder_name}' (ID: {folder_id_info})") # Identify folder type based on level and name patterns if level == 0: # This is the opportunity folder (the one we're analyzing) structure['opportunity_folder'] = { 'id': folder_id_info, 'name': folder_name, 'level': level, 'found': True } _logger.info(f"✅ Identified opportunity folder: {folder_name}") elif level == 1: # This could be year folder or primary folder if folder_name.isdigit() and len(folder_name) == 4: # It's a year folder structure['year_folder'] = { 'id': folder_id_info, 'name': folder_name, 'level': level, 'found': True } _logger.info(f"✅ Identified year folder: {folder_name}") elif folder_name not in ['Meets', 'Archivos cliente']: # It's a primary folder (company/contact) structure['primary_folder'] = { 'id': folder_id_info, 'name': folder_name, 'level': level, 'found': True } _logger.info(f"✅ Identified primary folder: {folder_name}") elif level == 2: # This could be year folder or primary folder (depending on what was at level 1) if folder_name.isdigit() and len(folder_name) == 4 and not structure['year_folder']['found']: # It's a year folder structure['year_folder'] = { 'id': folder_id_info, 'name': folder_name, 'level': level, 'found': True } _logger.info(f"✅ Identified year folder at level 2: {folder_name}") elif folder_name not in ['Meets', 'Archivos cliente'] and not structure['primary_folder']['found']: # It's a primary folder structure['primary_folder'] = { 'id': folder_id_info, 'name': folder_name, 'level': level, 'found': True } _logger.info(f"✅ Identified primary folder at level 2: {folder_name}") elif level >= 3: # This is likely the root folder if not structure['root_folder']['found']: structure['root_folder'] = { 'id': folder_id_info, 'name': folder_name, 'level': level, 'found': True } _logger.info(f"✅ Identified root folder: {folder_name}") _logger.info(f"📊 Final structure analysis: {structure}") return structure except Exception as e: _logger.error(f"Error in _analyze_crm_folder_structure: {str(e)}") return None # ============================================================================ # MÉTODOS DE ACCIÓN # ============================================================================ @api.model def create(self, vals_list): """Override create to handle Google Drive folder creation - supports batch creation""" # Handle both single record and batch creation if isinstance(vals_list, dict): vals_list = [vals_list] records = super().create(vals_list) # Process each record individually for Google Drive folder creation for record in records: 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): if record._validate_folder_creation_prerequisites(): try: record._create_google_drive_folder_structure() record._post_folder_message("✅ Google Drive folder created automatically") except Exception as e: _logger.error(f"Failed to create Google Drive folder for opportunity {record.id}: {str(e)}") record._post_folder_message(f"⚠️ Google Drive folder creation failed: {str(e)}") return records def write(self, vals): """Override write method to handle Google Drive folder updates""" if self.env.context.get('skip_google_drive_update'): return super().write(vals) _clear_google_drive_cache() 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 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 '' } result = super().write(vals) # Process Google Drive updates for record in self: record._process_google_drive_updates(vals, current_values[record.id]) return result 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')) return { 'type': 'ir.actions.act_url', 'url': f"https://drive.google.com/drive/folders/{self.google_drive_folder_id}", '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')) if not self._get_company_root_folder_id(): raise UserError(_('Google Drive CRM folder is not configured for this company.')) try: self._create_google_drive_folder_structure() 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_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.')) if not self._get_company_root_folder_id(): raise UserError(_('Google Drive CRM folder is not configured for this company.')) try: expected_components = self._get_current_folder_components() self._rename_entire_folder_structure(new_components=expected_components) self._update_folder_structure_fields(structure_components=expected_components) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Success'), 'message': _('Google Drive folder structure renamed successfully!'), '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.')) try: # Add detailed diagnostic information _logger.info(f"🔍 Analyzing folder structure for opportunity {self.id} (ID: {self.name})") _logger.info(f"📁 Current folder ID: {self.google_drive_folder_id}") expected_components = self._get_current_folder_components() _logger.info(f"📋 Expected components: {expected_components}") # Test folder validation first drive_service = self.env['google.drive.service'] validation = drive_service.validate_folder_id(self.google_drive_folder_id) _logger.info(f"✅ Folder validation result: {validation}") current_structure = self._analyze_crm_folder_structure(self.google_drive_folder_id) _logger.info(f"📊 Current structure analysis: {current_structure}") if not current_structure: # Provide a more helpful error message error_msg = f"❌ Cannot Analyze Folder Structure

" error_msg += f"Folder ID: {self.google_drive_folder_id}
" error_msg += f"Validation Result: {validation}
" error_msg += f"Possible Issues:
" error_msg += f"• Folder may not exist or be accessible
" error_msg += f"• Insufficient permissions to access the folder
" error_msg += f"• Folder may be in a Shared Drive without proper access
" error_msg += f"• Network connectivity issues

" error_msg += f"Expected Structure:
" error_msg += f"📁 [Root Folder] (MC Team)
" error_msg += f"└── 📁 {expected_components['primary_name']} (Company/Contact)
" error_msg += f" └── 📁 {expected_components['year']} (Year)
" error_msg += f" └── 📁 {expected_components['opportunity_name']} (Opportunity)
" error_msg += f" ├── 📁 Meets
" error_msg += f" └── 📁 Archivos cliente

" error_msg += f"Recommendation: Try using the 'Rename Folder Structure' button to recreate the structure." return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Folder Structure Analysis'), 'message': error_msg, 'type': 'warning', 'sticky': True, } } analysis = self._compare_folder_structures(expected_components, current_structure) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Folder Structure Analysis'), 'message': analysis, 'type': 'info', 'sticky': True, } } except Exception as e: _logger.error(f"Error in action_analyze_folder_structure: {str(e)}") raise UserError(_('Failed to analyze folder structure: %s') % str(e)) def _compare_folder_structures(self, expected_components, current_structure): """Compare expected vs current folder structure - EXTRA SPACING FORMAT""" try: # Create text analysis with EXTRA spacing for better readability analysis = "📁 FOLDER STRUCTURE ANALYSIS\n" analysis += "=" * 60 + "\n\n\n" # Expected structure analysis += "✅ EXPECTED STRUCTURE:\n" analysis += "=" * 30 + "\n\n" analysis += f"📁 [Root Folder] (MC Team)\n" analysis += f"└── 📁 {expected_components['primary_name']} (Company/Contact)\n" analysis += f" └── 📁 {expected_components['year']} (Year)\n" analysis += f" └── 📁 {expected_components['opportunity_name']} (Opportunity)\n" analysis += f" ├── 📁 Meets\n" analysis += f" └── 📁 Archivos cliente\n\n\n" # Current structure analysis += "🔍 CURRENT STRUCTURE:\n" analysis += "=" * 30 + "\n\n" # Root folder if current_structure.get('root_folder', {}).get('found', False): root_name = current_structure['root_folder']['name'] analysis += f"📁 {root_name} (Root) ✅\n" else: analysis += "📁 [Unknown Root] ❌\n" # Primary folder if current_structure.get('primary_folder', {}).get('found', False): primary_name = current_structure['primary_folder']['name'] if primary_name == expected_components['primary_name']: analysis += f"└── 📁 {primary_name} ✅\n" else: analysis += f"└── 📁 {primary_name} ❌\n" analysis += f" (Expected: {expected_components['primary_name']})\n" else: analysis += "└── 📁 [Missing Primary Folder] ❌\n" # Year folder if current_structure.get('year_folder', {}).get('found', False): year_name = current_structure['year_folder']['name'] if year_name == expected_components['year']: analysis += f" └── 📁 {year_name} ✅\n" else: analysis += f" └── 📁 {year_name} ❌\n" analysis += f" (Expected: {expected_components['year']})\n" else: analysis += " └── 📁 [Missing Year Folder] ❌\n" # Opportunity folder if current_structure.get('opportunity_folder', {}).get('found', False): opp_name = current_structure['opportunity_folder']['name'] if opp_name == expected_components['opportunity_name']: analysis += f" └── 📁 {opp_name} ✅\n" else: analysis += f" └── 📁 {opp_name} ❌\n" analysis += f" (Expected: {expected_components['opportunity_name']})\n" else: analysis += " └── 📁 [Missing Opportunity Folder] ❌\n" # Add subfolders to current structure (always show them) analysis += f" ├── 📁 Meets ✅\n" analysis += f" └── 📁 Archivos cliente ✅\n\n\n" # Calculate summary correct_count = 0 total_count = 0 # Check primary folder if current_structure.get('primary_folder', {}).get('found', False): total_count += 1 if current_structure['primary_folder']['name'] == expected_components['primary_name']: correct_count += 1 # Check year folder if current_structure.get('year_folder', {}).get('found', False): total_count += 1 if current_structure['year_folder']['name'] == expected_components['year']: correct_count += 1 # Check opportunity folder if current_structure.get('opportunity_folder', {}).get('found', False): total_count += 1 if current_structure['opportunity_folder']['name'] == expected_components['opportunity_name']: correct_count += 1 # Summary section with EXTRA spacing analysis += "=" * 60 + "\n\n" analysis += "📊 SUMMARY:\n" analysis += "=" * 20 + "\n\n" if total_count == 3 and correct_count == 3: analysis += "🎉 PERFECT STRUCTURE!\n\n" analysis += "✅ All folders are in the right place\n" analysis += "✅ All folder names are correct\n" analysis += f"✅ {correct_count}/{total_count} Components Correct\n\n" analysis += "🎯 Status: Ready to use!\n" else: analysis += "⚠️ STRUCTURE ISSUES DETECTED\n\n" analysis += f"❌ Structure has issues ({correct_count}/{total_count} correct)\n\n" analysis += "💡 RECOMMENDATION:\n" analysis += "Use the 'Rename Folder Structure' button to fix the folder hierarchy.\n\n" analysis += "🔧 Action Required: Manual intervention needed\n" return analysis except Exception as e: _logger.error(f"Error in _compare_folder_structures: {str(e)}") return f"❌ ERROR ANALYZING FOLDER STRUCTURE\n{str(e)}" # ============================================================================ # MÉTODOS DE SINCRONIZACIÓN DE MEETS # ============================================================================ @api.model def _sync_meetings_with_opportunities(self, time_filter=15): """Sync Google Meet recordings with CRM opportunities Args: time_filter: Can be: - int: Number of days back (e.g., 15 for last 15 days) - str: Specific date in YYYY-MM-DD format (e.g., '2024-01-15' for that specific day) """ # Parse time_filter parameter if isinstance(time_filter, str): # Try to parse as date try: from datetime import datetime target_date = datetime.strptime(time_filter, '%Y-%m-%d').date() _logger.info(f"Starting CRM meetings synchronization for specific date: {time_filter}") days_back = None specific_date = target_date except ValueError: # If not a valid date, try to convert to int try: days_back = int(time_filter) specific_date = None _logger.info(f"Starting CRM meetings synchronization for last {days_back} days...") except ValueError: raise UserError(_('Invalid time_filter parameter. Use integer (days back) or date string (YYYY-MM-DD)')) else: # Assume it's an integer days_back = int(time_filter) specific_date = None _logger.info(f"Starting CRM meetings synchronization for last {days_back} days...") try: # Get Google Drive service drive_service = self.env['google.drive.service'] # Get meetings based on filter type if specific_date: meetings = self._get_meetings_with_recordings_for_date(specific_date) else: meetings = self._get_meetings_with_recordings(days_back=days_back) _logger.info(f"Found {len(meetings)} meetings with recordings") # Limit to first 10 meetings for performance meetings = meetings[:10] _logger.info(f"Processing first {len(meetings)} meetings for performance") sync_results = { 'total_meetings': len(meetings), 'opportunities_found': 0, 'folders_created': 0, 'files_moved': 0, 'errors': [] } for meeting in meetings: try: result = self._process_meeting_for_opportunities(meeting, drive_service) sync_results['opportunities_found'] += result.get('opportunities_found', 0) sync_results['folders_created'] += result.get('folders_created', 0) sync_results['files_moved'] += result.get('files_moved', 0) except Exception as e: error_msg = f"Error processing meeting {meeting.get('id', 'unknown')}: {str(e)}" _logger.error(error_msg) sync_results['errors'].append(error_msg) # Generate summary message summary = self._generate_sync_summary(sync_results) _logger.info(f"CRM sync completed: {summary}") return summary except Exception as e: _logger.error(f"Failed to sync meetings with opportunities: {str(e)}") raise UserError(_('Failed to sync meetings: %s') % str(e)) @api.model def _sync_meetings_with_opportunities_cron(self, time_filter=15): """Cron job method for automatic CRM calendar sync - handles errors gracefully Args: time_filter: Can be: - int: Number of days back (e.g., 15 for last 15 days) - str: Specific date in YYYY-MM-DD format (e.g., '2024-01-15' for that specific day) """ # Parse time_filter parameter for logging if isinstance(time_filter, str): try: from datetime import datetime target_date = datetime.strptime(time_filter, '%Y-%m-%d').date() log_message = f"🔄 Starting automatic CRM calendar sync (cron) for specific date: {time_filter}" except ValueError: try: days_back = int(time_filter) log_message = f"🔄 Starting automatic CRM calendar sync (cron) for last {days_back} days..." except ValueError: log_message = f"🔄 Starting automatic CRM calendar sync (cron) with invalid time_filter: {time_filter}" else: days_back = int(time_filter) log_message = f"🔄 Starting automatic CRM calendar sync (cron) for last {days_back} days..." _logger.info(log_message) try: # Check if any company has Google Drive CRM enabled companies_with_google = self.env['res.company'].search([ ('google_drive_crm_enabled', '=', True), ('google_drive_crm_folder_id', '!=', False) ]) if not companies_with_google: _logger.info("🔄 No companies configured for Google Drive CRM. Skipping sync.") return # Check if there are any opportunities to process opportunities_count = self.env['crm.lead'].search_count([ ('type', '=', 'opportunity'), ('stage_id.is_won', '=', False) ]) if opportunities_count == 0: _logger.info("🔄 No active opportunities found. Skipping sync.") return # Find users with Google authentication authenticated_users = self.env['res.users'].search([ ('res_users_settings_id.google_rtoken', '!=', False), ('res_users_settings_id.google_token', '!=', False) ]) if not authenticated_users: _logger.warning("🔄 No users with Google authentication found. Skipping sync.") return _logger.info(f"🔄 Found {len(authenticated_users)} users with Google authentication") # Execute sync for each authenticated user total_results = { 'total_meetings': 0, 'opportunities_found': 0, 'folders_created': 0, 'files_moved': 0, 'errors': [] } for user in authenticated_users: try: _logger.info(f"🔄 Executing sync for user: {user.name} ({user.login})") # Execute sync with user context result = self.with_user(user.id)._sync_meetings_with_opportunities(time_filter=time_filter) # Parse result if it's a string (summary) if isinstance(result, str): # Extract numbers from summary (basic parsing) import re meetings_match = re.search(r'Reuniones procesadas: (\d+)', result) opportunities_match = re.search(r'Oportunidades encontradas: (\d+)', result) folders_match = re.search(r'Carpetas creadas: (\d+)', result) files_match = re.search(r'Archivos movidos: (\d+)', result) if meetings_match: total_results['total_meetings'] += int(meetings_match.group(1)) if opportunities_match: total_results['opportunities_found'] += int(opportunities_match.group(1)) if folders_match: total_results['folders_created'] += int(folders_match.group(1)) if files_match: total_results['files_moved'] += int(files_match.group(1)) _logger.info(f"✅ Sync completed for user {user.name}") except Exception as e: error_msg = f"Error syncing for user {user.name}: {str(e)}" _logger.error(error_msg) total_results['errors'].append(error_msg) continue # Generate final summary final_summary = self._generate_sync_summary(total_results) _logger.info(f"✅ Automatic CRM calendar sync completed for all users: {final_summary}") return final_summary except Exception as e: # Log error but don't fail the cron job _logger.error(f"❌ Automatic CRM calendar sync failed: {str(e)}") # Create a system message for administrators try: admin_users = self.env['res.users'].search([('groups_id', 'in', self.env.ref('base.group_system').id)]) for admin in admin_users: admin.message_post( body=f"❌ CRM Calendar Sync Error
" f"Error: {str(e)}
" f"Time: {fields.Datetime.now()}
" f"Time Filter: {time_filter}", message_type='comment', subtype_xmlid='mail.mt_comment' ) except: pass # Don't fail if we can't create messages return False def _get_meetings_with_recordings(self, days_back=15): """Get Google Meet meetings from last N days that have recordings""" try: # Use Google Calendar service to get real meetings calendar_service = self.env['google.calendar.service'] meetings = calendar_service.get_meetings_with_recordings(days_back=days_back) _logger.info(f"Retrieved {len(meetings)} meetings with recordings from Google Calendar") return meetings except Exception as e: _logger.error(f"Failed to get meetings from Google Calendar: {str(e)}") # Fallback to empty list if API fails return [] def _get_meetings_with_recordings_for_date(self, target_date): """Get Google Meet meetings from a specific date that have recordings""" try: # Use Google Calendar service to get meetings for specific date calendar_service = self.env['google.calendar.service'] meetings = calendar_service.get_meetings_with_recordings_for_date(target_date) _logger.info(f"Retrieved {len(meetings)} meetings with recordings from Google Calendar for date: {target_date}") return meetings except Exception as e: _logger.error(f"Failed to get meetings from Google Calendar for date {target_date}: {str(e)}") # Fallback to empty list if API fails return [] def _process_meeting_for_opportunities(self, meeting, drive_service): """Process a single meeting for opportunities""" result = { 'opportunities_found': 0, 'folders_created': 0, 'files_moved': 0 } # Find opportunities by participant emails opportunities = self._find_opportunities_by_participants(meeting['participants']) result['opportunities_found'] = len(opportunities) if not opportunities: _logger.info(f"❌ No opportunities found for meeting '{meeting['title']}' with participants: {meeting['participants']}") return result for opportunity in opportunities: try: _logger.info(f"🎯 Processing meeting '{meeting['title']}' for opportunity '{opportunity.name}' (ID: {opportunity.id})") # Ensure opportunity has Google Drive folder structure if not opportunity.google_drive_folder_id: _logger.info(f"📁 Creating Google Drive folder for opportunity {opportunity.name}") opportunity._create_google_drive_folder_structure() result['folders_created'] += 1 # Get or create Meets folder meets_folder_id = self._get_or_create_meets_folder(opportunity, drive_service) # Move recording files to Meets folder files_moved = self._move_recording_files_to_meets_folder( meeting['recording_files'], meets_folder_id, drive_service, meeting['title'] ) result['files_moved'] += files_moved # Search for additional files in the user's configured CRM meets folder additional_files_moved = self._search_and_move_crm_meets_files(meeting['title'], meets_folder_id, drive_service) result['files_moved'] += additional_files_moved _logger.info(f"✅ Successfully processed meeting '{meeting['title']}' for opportunity '{opportunity.name}': {files_moved + additional_files_moved} files moved") except Exception as e: _logger.error(f"❌ Error processing opportunity {opportunity.name}: {str(e)}") continue return result def _find_opportunities_by_participants(self, participant_emails): """Find opportunities where participants are partners - returns the most recent one""" opportunities = self.env['crm.lead'].search([ ('partner_id.email', 'in', participant_emails), ('type', '=', 'opportunity'), ('stage_id.is_won', '=', False) # Only active opportunities ], order='create_date desc', limit=1) if opportunities: opportunity = opportunities[0] _logger.info(f"Found most recent opportunity '{opportunity.name}' (ID: {opportunity.id}, created: {opportunity.create_date}) for participants: {participant_emails}") else: _logger.info(f"No opportunities found for participants: {participant_emails}") return opportunities def _get_or_create_meets_folder(self, opportunity, drive_service): """Get or create Meets folder within opportunity folder""" try: # List folders in opportunity folder folders = drive_service._do_request( '/drive/v3/files', params={ 'q': f"'{opportunity.google_drive_folder_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false", 'fields': 'files(id,name)', 'supportsAllDrives': 'true', 'includeItemsFromAllDrives': 'true' } ) # Look for existing Meets folder meets_folder_id = None for folder in folders.get('files', []): if folder['name'] == 'Meets': meets_folder_id = folder['id'] _logger.info(f"Found existing Meets folder: {meets_folder_id}") break # Create Meets folder if it doesn't exist if not meets_folder_id: _logger.info(f"Creating Meets folder for opportunity {opportunity.name}") meets_folder = drive_service._do_request( '/drive/v3/files', method='POST', json_data={ 'name': 'Meets', 'mimeType': 'application/vnd.google-apps.folder', 'parents': [opportunity.google_drive_folder_id] } ) meets_folder_id = meets_folder['id'] _logger.info(f"Created Meets folder: {meets_folder_id}") return meets_folder_id except Exception as e: _logger.error(f"Error getting/creating Meets folder: {str(e)}") raise def _move_recording_files_to_meets_folder(self, file_ids, meets_folder_id, drive_service, meeting_title): """Move recording files to Meets folder""" files_moved = 0 _logger.info(f"🔍 Starting to move {len(file_ids)} files to Meets folder: {meets_folder_id}") _logger.info(f"📁 Target Meets folder ID: {meets_folder_id}") for file_id in file_ids: try: _logger.info(f"📄 Processing file ID: {file_id}") # Check if file is already in the Meets folder file_info = drive_service._do_request( f'/drive/v3/files/{file_id}', params={ 'fields': 'id,name,parents', 'supportsAllDrives': 'true', 'includeItemsFromAllDrives': 'true' } ) file_name = file_info.get('name', 'Unknown') current_parents = file_info.get('parents', []) _logger.info(f"📄 File: {file_name} (ID: {file_id})") _logger.info(f"📍 Current parents: {current_parents}") _logger.info(f"🎯 Target folder: {meets_folder_id}") # Check if file is already in the target folder if meets_folder_id in current_parents: _logger.info(f"✅ File {file_name} already in Meets folder") continue # Move file to Meets folder _logger.info(f"🚚 Moving file {file_name} to Meets folder...") # Prepare parameters for Shared Drive support move_params = { 'supportsAllDrives': 'true', 'includeItemsFromAllDrives': 'true' } # Try alternative approach: use addParents and removeParents as URL parameters move_params['addParents'] = meets_folder_id move_params['removeParents'] = ','.join(current_parents) _logger.info(f"🔧 Using URL parameters: addParents={meets_folder_id}, removeParents={','.join(current_parents)}") drive_service._do_request( f'/drive/v3/files/{file_id}', method='PATCH', params=move_params ) # Verify the move was successful updated_file_info = drive_service._do_request( f'/drive/v3/files/{file_id}', params={ 'fields': 'id,name,parents', 'supportsAllDrives': 'true', 'includeItemsFromAllDrives': 'true' } ) updated_parents = updated_file_info.get('parents', []) _logger.info(f"✅ File moved! New parents: {updated_parents}") files_moved += 1 _logger.info(f"✅ Successfully moved file {file_name} to Meets folder for meeting: {meeting_title}") except Exception as e: _logger.error(f"❌ Error moving file {file_id}: {str(e)}") continue _logger.info(f"📊 Total files moved: {files_moved} out of {len(file_ids)}") return files_moved def _search_and_move_crm_meets_files(self, meeting_title, meets_folder_id, drive_service): """Search for files in the user's configured CRM meets folder and move them to the opportunity's Meets folder""" files_moved = 0 try: # Get the current user's CRM meets folder configuration current_user = self.env.user user_settings = current_user.res_users_settings_id if not user_settings or not user_settings.google_crm_meets_folder_id: _logger.info(f"🔍 No CRM meets folder configured for user {current_user.name}. Skipping additional file search.") return 0 crm_meets_folder_id = user_settings.google_crm_meets_folder_id _logger.info(f"🔍 Searching for files in user's CRM meets folder ({crm_meets_folder_id}) containing meeting title: '{meeting_title}'") # Search for files in the user's CRM meets folder that contain the meeting title files_in_crm_meets = drive_service._do_request( '/drive/v3/files', params={ 'q': f"'{crm_meets_folder_id}' in parents and trashed=false and name contains '{meeting_title}'", 'fields': 'files(id,name,parents)', 'supportsAllDrives': 'true', 'includeItemsFromAllDrives': 'true' } ) found_files = files_in_crm_meets.get('files', []) _logger.info(f"🔍 Found {len(found_files)} files in CRM meets folder containing meeting title: '{meeting_title}'") for file_info in found_files: file_id = file_info['id'] file_name = file_info['name'] current_parents = file_info.get('parents', []) _logger.info(f"📄 Processing file '{file_name}' (ID: {file_id}) from CRM meets folder") # Check if file is already in the target folder if meets_folder_id in current_parents: _logger.info(f"✅ File '{file_name}' already in opportunity's Meets folder") continue # Move file to the opportunity's Meets folder _logger.info(f"🚚 Moving file '{file_name}' to opportunity's Meets folder...") # Prepare parameters for Shared Drive support move_params = { 'supportsAllDrives': 'true', 'includeItemsFromAllDrives': 'true' } # Try alternative approach: use addParents and removeParents as URL parameters move_params['addParents'] = meets_folder_id move_params['removeParents'] = ','.join(current_parents) _logger.info(f"🔧 Using URL parameters: addParents={meets_folder_id}, removeParents={','.join(current_parents)}") drive_service._do_request( f'/drive/v3/files/{file_id}', method='PATCH', params=move_params ) # Verify the move was successful updated_file_info = drive_service._do_request( f'/drive/v3/files/{file_id}', params={ 'fields': 'id,name,parents', 'supportsAllDrives': 'true', 'includeItemsFromAllDrives': 'true' } ) updated_parents = updated_file_info.get('parents', []) _logger.info(f"✅ File moved! New parents: {updated_parents}") files_moved += 1 _logger.info(f"✅ Successfully moved file '{file_name}' to opportunity's Meets folder for meeting: {meeting_title}") _logger.info(f"📊 Total additional files moved from CRM meets folder: {files_moved}") return files_moved except Exception as e: _logger.error(f"❌ Error searching or moving files from CRM meets folder: {str(e)}") return 0 def _generate_sync_summary(self, sync_results): """Generate summary message for sync results""" summary = f"🔄 Sincronización CRM Completada

" summary += f"📊 Resumen:
" summary += f"• Reuniones procesadas: {sync_results['total_meetings']}
" summary += f"• Oportunidades encontradas: {sync_results['opportunities_found']}
" summary += f"• Carpetas creadas: {sync_results['folders_created']}
" summary += f"• Archivos movidos: {sync_results['files_moved']}
" if sync_results['errors']: summary += f"
Errores ({len(sync_results['errors'])}):
" for error in sync_results['errors'][:5]: # Show first 5 errors summary += f"• {error}
" if len(sync_results['errors']) > 5: summary += f"• ... y {len(sync_results['errors']) - 5} errores más
" return summary